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(v) = patch.h2c_prior_knowledge {
1640 listener.h2c_prior_knowledge = Some(v);
1641 }
1642 if let Some(patch_answers) = patch.http_answers.as_ref() {
1643 merge_custom_http_answers(&mut listener.http_answers, patch_answers);
1644 }
1645 merge_answers(&mut listener.answers, &patch.answers);
1646 if let Some(v) = patch.h2_max_rst_stream_per_window {
1648 listener.h2_max_rst_stream_per_window = Some(v);
1649 }
1650 if let Some(v) = patch.h2_max_ping_per_window {
1651 listener.h2_max_ping_per_window = Some(v);
1652 }
1653 if let Some(v) = patch.h2_max_settings_per_window {
1654 listener.h2_max_settings_per_window = Some(v);
1655 }
1656 if let Some(v) = patch.h2_max_empty_data_per_window {
1657 listener.h2_max_empty_data_per_window = Some(v);
1658 }
1659 if let Some(v) = patch.h2_max_continuation_frames {
1660 listener.h2_max_continuation_frames = Some(v);
1661 }
1662 if let Some(v) = patch.h2_max_glitch_count {
1663 listener.h2_max_glitch_count = Some(v);
1664 }
1665 if let Some(v) = patch.h2_initial_connection_window {
1666 listener.h2_initial_connection_window = Some(v);
1667 }
1668 if let Some(v) = patch.h2_max_concurrent_streams {
1669 listener.h2_max_concurrent_streams = Some(v);
1670 }
1671 if let Some(v) = patch.h2_stream_shrink_ratio {
1672 listener.h2_stream_shrink_ratio = Some(v);
1673 }
1674 if let Some(v) = patch.h2_max_rst_stream_lifetime {
1675 listener.h2_max_rst_stream_lifetime = Some(v);
1676 }
1677 if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
1678 listener.h2_max_rst_stream_abusive_lifetime = Some(v);
1679 }
1680 if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
1681 listener.h2_max_rst_stream_emitted_lifetime = Some(v);
1682 }
1683 if let Some(v) = patch.h2_max_header_list_size {
1684 listener.h2_max_header_list_size = Some(v);
1685 }
1686 if let Some(v) = patch.h2_max_header_table_size {
1687 listener.h2_max_header_table_size = Some(v);
1688 }
1689 if let Some(v) = patch.h2_max_header_fields {
1690 listener.h2_max_header_fields = Some(v);
1691 }
1692 if let Some(v) = patch.h2_stream_refusal_percent {
1693 listener.h2_stream_refusal_percent = Some(v);
1694 }
1695 if let Some(v) = patch.h2_stream_idle_timeout_seconds {
1696 listener.h2_stream_idle_timeout_seconds = Some(v);
1697 }
1698 if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
1700 listener.h2_graceful_shutdown_deadline_seconds = Some(v);
1701 }
1702 if let Some(v) = patch.h2_max_window_update_stream0_per_window {
1703 listener.h2_max_window_update_stream0_per_window = Some(v);
1704 }
1705 if let Some(ref v) = patch.sozu_id_header {
1706 listener.sozu_id_header = Some(v.to_owned());
1707 }
1708 if let Some(v) = patch.forwarded_headers {
1709 listener.forwarded_headers = Some(v);
1710 }
1711 Ok(())
1712 }
1713
1714 pub fn client_auth_refresh(
1724 &self,
1725 listener: &HttpsListenerConfig,
1726 ) -> Option<UpdateHttpsListenerConfig> {
1727 let running = self.https_listeners.get(&listener.listener_key())?;
1728 let policy = listener.client_auth_policy();
1729 (running.client_auth_policy() != policy).then(|| UpdateHttpsListenerConfig {
1730 address: listener.address,
1731 client_auth_policy: Some(policy),
1732 ..Default::default()
1733 })
1734 }
1735
1736 fn update_https_listener(
1756 &mut self,
1757 patch: &UpdateHttpsListenerConfig,
1758 ) -> Result<(), StateError> {
1759 validate_h2_flood_knobs_https(patch)?;
1760
1761 let key = listener_key_for_update(
1762 &self.https_listeners,
1763 patch.address.into(),
1764 ObjectKind::HttpsListener,
1765 )?;
1766 if let Some(ref alpn_wrapper) = patch.alpn_protocols {
1767 validate_alpn_protocols(&alpn_wrapper.values)?;
1768 }
1769 if let Some(ref v) = patch.sozu_id_header {
1770 validate_sozu_id_header(v)?;
1771 }
1772 if let Some(v) = patch.forwarded_headers {
1773 validate_forwarded_headers(v)?;
1774 }
1775 if let Some(ref hsts) = patch.hsts {
1776 validate_hsts_patch(hsts)?;
1777 }
1778
1779 let listener = self
1780 .https_listeners
1781 .get_mut(&key)
1782 .expect("listener_key_for_update returns a key present in the map");
1783
1784 if let Some(v) = patch.public_address {
1786 listener.public_address = Some(v);
1787 }
1788 if let Some(v) = patch.expect_proxy {
1789 listener.expect_proxy = v;
1790 }
1791 if let Some(ref v) = patch.sticky_name {
1792 listener.sticky_name = v.to_owned();
1793 }
1794 if let Some(v) = patch.front_timeout {
1795 listener.front_timeout = v;
1796 }
1797 if let Some(v) = patch.back_timeout {
1798 listener.back_timeout = v;
1799 }
1800 if let Some(v) = patch.connect_timeout {
1801 listener.connect_timeout = v;
1802 }
1803 if let Some(v) = patch.request_timeout {
1804 listener.request_timeout = v;
1805 }
1806 if let Some(v) = patch.elide_x_real_ip {
1807 listener.elide_x_real_ip = Some(v);
1808 }
1809 if let Some(v) = patch.send_x_real_ip {
1810 listener.send_x_real_ip = Some(v);
1811 }
1812 if let Some(patch_answers) = patch.http_answers.as_ref() {
1813 merge_custom_http_answers(&mut listener.http_answers, patch_answers);
1814 }
1815 merge_answers(&mut listener.answers, &patch.answers);
1816 if let Some(hsts) = patch.hsts {
1819 listener.hsts = Some(hsts);
1820 }
1821 if let Some(ref alpn_wrapper) = patch.alpn_protocols {
1822 listener.alpn_protocols = alpn_wrapper.values.clone();
1824 }
1825 if let Some(v) = patch.strict_sni_binding {
1826 listener.strict_sni_binding = Some(v);
1827 }
1828 if let Some(v) = patch.disable_http11 {
1829 listener.disable_http11 = Some(v);
1830 }
1831 if let Some(ref policy) = patch.client_auth_policy {
1832 listener.apply_client_auth_policy(policy);
1833 }
1834 if let Some(v) = patch.h2_max_rst_stream_per_window {
1836 listener.h2_max_rst_stream_per_window = Some(v);
1837 }
1838 if let Some(v) = patch.h2_max_ping_per_window {
1839 listener.h2_max_ping_per_window = Some(v);
1840 }
1841 if let Some(v) = patch.h2_max_settings_per_window {
1842 listener.h2_max_settings_per_window = Some(v);
1843 }
1844 if let Some(v) = patch.h2_max_empty_data_per_window {
1845 listener.h2_max_empty_data_per_window = Some(v);
1846 }
1847 if let Some(v) = patch.h2_max_continuation_frames {
1848 listener.h2_max_continuation_frames = Some(v);
1849 }
1850 if let Some(v) = patch.h2_max_glitch_count {
1851 listener.h2_max_glitch_count = Some(v);
1852 }
1853 if let Some(v) = patch.h2_initial_connection_window {
1854 listener.h2_initial_connection_window = Some(v);
1855 }
1856 if let Some(v) = patch.h2_max_concurrent_streams {
1857 listener.h2_max_concurrent_streams = Some(v);
1858 }
1859 if let Some(v) = patch.h2_stream_shrink_ratio {
1860 listener.h2_stream_shrink_ratio = Some(v);
1861 }
1862 if let Some(v) = patch.h2_max_rst_stream_lifetime {
1863 listener.h2_max_rst_stream_lifetime = Some(v);
1864 }
1865 if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
1866 listener.h2_max_rst_stream_abusive_lifetime = Some(v);
1867 }
1868 if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
1869 listener.h2_max_rst_stream_emitted_lifetime = Some(v);
1870 }
1871 if let Some(v) = patch.h2_max_header_list_size {
1872 listener.h2_max_header_list_size = Some(v);
1873 }
1874 if let Some(v) = patch.h2_max_header_table_size {
1875 listener.h2_max_header_table_size = Some(v);
1876 }
1877 if let Some(v) = patch.h2_max_header_fields {
1878 listener.h2_max_header_fields = Some(v);
1879 }
1880 if let Some(v) = patch.h2_stream_refusal_percent {
1881 listener.h2_stream_refusal_percent = Some(v);
1882 }
1883 if let Some(v) = patch.h2_stream_idle_timeout_seconds {
1884 listener.h2_stream_idle_timeout_seconds = Some(v);
1885 }
1886 if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
1888 listener.h2_graceful_shutdown_deadline_seconds = Some(v);
1889 }
1890 if let Some(v) = patch.h2_max_window_update_stream0_per_window {
1891 listener.h2_max_window_update_stream0_per_window = Some(v);
1892 }
1893 if let Some(ref v) = patch.sozu_id_header {
1894 listener.sozu_id_header = Some(v.to_owned());
1895 }
1896 if let Some(v) = patch.forwarded_headers {
1897 listener.forwarded_headers = Some(v);
1898 }
1899 Ok(())
1900 }
1901
1902 fn update_tcp_listener(&mut self, patch: &UpdateTcpListenerConfig) -> Result<(), StateError> {
1907 let key = listener_key_for_update(
1908 &self.tcp_listeners,
1909 patch.address.into(),
1910 ObjectKind::TcpListener,
1911 )?;
1912 let listener = self
1913 .tcp_listeners
1914 .get_mut(&key)
1915 .expect("listener_key_for_update returns a key present in the map");
1916
1917 if let Some(v) = patch.public_address {
1918 listener.public_address = Some(v);
1919 }
1920 if let Some(v) = patch.expect_proxy {
1921 listener.expect_proxy = v;
1922 }
1923 if let Some(v) = patch.front_timeout {
1924 listener.front_timeout = v;
1925 }
1926 if let Some(v) = patch.back_timeout {
1927 listener.back_timeout = v;
1928 }
1929 if let Some(v) = patch.connect_timeout {
1930 listener.connect_timeout = v;
1931 }
1932 Ok(())
1933 }
1934
1935 fn update_udp_listener(&mut self, patch: &UpdateUdpListenerConfig) -> Result<(), StateError> {
1940 let key = listener_key_for_update(
1941 &self.udp_listeners,
1942 patch.address.into(),
1943 ObjectKind::UdpListener,
1944 )?;
1945 let listener = self
1946 .udp_listeners
1947 .get_mut(&key)
1948 .expect("listener_key_for_update returns a key present in the map");
1949
1950 if let Some(v) = patch.public_address {
1951 listener.public_address = Some(v);
1952 }
1953 if let Some(v) = patch.front_timeout {
1954 listener.front_timeout = v;
1955 }
1956 if let Some(v) = patch.back_timeout {
1957 listener.back_timeout = v;
1958 }
1959 if let Some(v) = patch.max_rx_datagram_size {
1960 listener.max_rx_datagram_size = v;
1961 }
1962 if let Some(v) = patch.max_flows {
1963 listener.max_flows = v;
1964 }
1965 Ok(())
1966 }
1967
1968 fn activate_listener(&mut self, activate: &ActivateListener) -> Result<(), StateError> {
1969 match ListenerType::try_from(activate.proxy).map_err(StateError::WrongFieldValue)? {
1970 ListenerType::Http => self
1971 .http_listeners
1972 .get_mut(&activate.listener_key())
1973 .map(|listener| listener.active = true)
1974 .ok_or(StateError::NotFound {
1975 kind: ObjectKind::HttpListener,
1976 id: activate.listener_key().to_string(),
1977 }),
1978 ListenerType::Https => self
1979 .https_listeners
1980 .get_mut(&activate.listener_key())
1981 .map(|listener| listener.active = true)
1982 .ok_or(StateError::NotFound {
1983 kind: ObjectKind::HttpsListener,
1984 id: activate.listener_key().to_string(),
1985 }),
1986 ListenerType::Tcp => self
1987 .tcp_listeners
1988 .get_mut(&activate.listener_key())
1989 .map(|listener| listener.active = true)
1990 .ok_or(StateError::NotFound {
1991 kind: ObjectKind::TcpListener,
1992 id: activate.listener_key().to_string(),
1993 }),
1994 ListenerType::Udp => self
1995 .udp_listeners
1996 .get_mut(&activate.listener_key())
1997 .map(|listener| listener.active = true)
1998 .ok_or(StateError::NotFound {
1999 kind: ObjectKind::UdpListener,
2000 id: activate.listener_key().to_string(),
2001 }),
2002 }
2003 }
2004
2005 fn deactivate_listener(&mut self, deactivate: &DeactivateListener) -> Result<(), StateError> {
2006 match ListenerType::try_from(deactivate.proxy).map_err(StateError::WrongFieldValue)? {
2007 ListenerType::Http => self
2008 .http_listeners
2009 .get_mut(&deactivate.listener_key())
2010 .map(|listener| listener.active = false)
2011 .ok_or(StateError::NotFound {
2012 kind: ObjectKind::HttpListener,
2013 id: deactivate.listener_key().to_string(),
2014 }),
2015 ListenerType::Https => self
2016 .https_listeners
2017 .get_mut(&deactivate.listener_key())
2018 .map(|listener| listener.active = false)
2019 .ok_or(StateError::NotFound {
2020 kind: ObjectKind::HttpsListener,
2021 id: deactivate.listener_key().to_string(),
2022 }),
2023 ListenerType::Tcp => self
2024 .tcp_listeners
2025 .get_mut(&deactivate.listener_key())
2026 .map(|listener| listener.active = false)
2027 .ok_or(StateError::NotFound {
2028 kind: ObjectKind::TcpListener,
2029 id: deactivate.listener_key().to_string(),
2030 }),
2031 ListenerType::Udp => self
2032 .udp_listeners
2033 .get_mut(&deactivate.listener_key())
2034 .map(|listener| listener.active = false)
2035 .ok_or(StateError::NotFound {
2036 kind: ObjectKind::UdpListener,
2037 id: deactivate.listener_key().to_string(),
2038 }),
2039 }
2040 }
2041
2042 fn add_frontend_to(
2049 fronts: &mut BTreeMap<String, HttpFrontend>,
2050 front: &RequestHttpFrontend,
2051 kind: ObjectKind,
2052 ) -> Result<(), StateError> {
2053 let front_as_key = front.to_string();
2054 let before = fronts.len();
2055
2056 match fronts.entry(front.to_string()) {
2057 BTreeMapEntry::Vacant(e) => {
2058 e.insert(front.clone().to_frontend().map_err(|into_error| {
2059 StateError::FrontendConversion {
2060 frontend: front_as_key,
2061 error: into_error.to_string(),
2062 }
2063 })?)
2064 }
2065 BTreeMapEntry::Occupied(_) => {
2066 debug_assert_eq!(
2067 fronts.len(),
2068 before,
2069 "a rejected duplicate {kind:?} add must not mutate the map"
2070 );
2071 return Err(StateError::Exists {
2072 kind,
2073 id: front.to_string(),
2074 });
2075 }
2076 };
2077 debug_assert!(
2080 fronts.contains_key(&front.to_string()),
2081 "adding a {kind:?} must insert the route key on success"
2082 );
2083 debug_assert_eq!(
2084 fronts.len(),
2085 before + 1,
2086 "adding a {kind:?} inserts exactly one entry on success"
2087 );
2088 Ok(())
2089 }
2090
2091 fn remove_frontend_from(
2094 fronts: &mut BTreeMap<String, HttpFrontend>,
2095 front: &RequestHttpFrontend,
2096 kind: ObjectKind,
2097 ) -> Result<(), StateError> {
2098 let key = front.to_string();
2099 let before = fronts.len();
2100 match fronts.get(&key) {
2105 Some(stored) if stored.cluster_id.as_deref() == front.cluster_id.as_deref() => {
2106 fronts.remove(&key);
2107 }
2108 _ => {
2109 debug_assert_eq!(fronts.len(), before, "a refused removal must not mutate");
2110 return Err(StateError::NotFound {
2111 kind,
2112 id: front.to_string(),
2113 });
2114 }
2115 }
2116 debug_assert!(
2117 !fronts.contains_key(&key),
2118 "removing a frontend must evict the route key"
2119 );
2120 debug_assert_eq!(
2121 fronts.len(),
2122 before - 1,
2123 "removing a frontend drops exactly one entry"
2124 );
2125 Ok(())
2126 }
2127
2128 fn add_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2129 Self::add_frontend_to(&mut self.http_fronts, front, ObjectKind::HttpFrontend)
2130 }
2131
2132 fn add_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2133 Self::add_frontend_to(&mut self.https_fronts, front, ObjectKind::HttpsFrontend)
2134 }
2135
2136 fn remove_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2137 Self::remove_frontend_from(&mut self.http_fronts, front, ObjectKind::HttpFrontend)
2138 }
2139
2140 fn remove_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2141 Self::remove_frontend_from(&mut self.https_fronts, front, ObjectKind::HttpsFrontend)
2142 }
2143
2144 fn add_certificate(&mut self, add: &AddCertificate) -> Result<(), StateError> {
2145 let fingerprint = add
2146 .certificate
2147 .fingerprint()
2148 .map_err(StateError::AddCertificate)?;
2149 let address: SocketAddr = add.address.into();
2150
2151 if self
2155 .certificates
2156 .get(&address)
2157 .is_some_and(|entries| entries.contains_key(&fingerprint))
2158 {
2159 let names_bytes = add
2160 .certificate
2161 .names
2162 .iter()
2163 .fold(0usize, |total, name| total.saturating_add(name.len()));
2164 info!(
2165 "Skip loading certificate with fingerprint_bytes={} names_count={} names_bytes={} on listener '{}', the certificate is already present.",
2166 fingerprint.0.len(),
2167 add.certificate.names.len(),
2168 names_bytes,
2169 add.address
2170 );
2171 return Ok(());
2172 }
2173
2174 let mut add = add.clone();
2185 add.certificate
2186 .apply_overriding_names()
2187 .map_err(StateError::AddCertificate)?;
2188 let trusted_next_order = self.certificate_metadata_is_trusted(address).then(|| {
2189 self.certificate_metadata
2190 .get(&address)
2191 .and_then(|index| index.next_order)
2192 .expect("trusted metadata index must carry its next ordinal")
2193 });
2194 let normalized_metadata = trusted_next_order
2195 .is_none()
2196 .then(|| self.normalized_certificate_metadata_for_address(address));
2197 let entry = self.certificates.entry(address).or_default();
2198 let before = entry.len();
2199 entry.insert(fingerprint.clone(), add.certificate);
2200 if let Some(insertion_order) = trusted_next_order {
2201 let metadata = self
2202 .certificate_metadata
2203 .get_mut(&address)
2204 .expect("trusted metadata index must remain present");
2205 metadata.entries.insert(
2206 fingerprint.clone(),
2207 CertificateMetadata {
2208 expired_at: add.expired_at,
2209 insertion_order,
2210 },
2211 );
2212 metadata.next_order = Some(insertion_order + 1);
2213 } else {
2214 let mut metadata =
2218 normalized_metadata.expect("untrusted metadata must have a normalized candidate");
2219 let insertion_order = metadata.entries.len() as u64;
2220 metadata.entries.insert(
2221 fingerprint.clone(),
2222 CertificateMetadata {
2223 expired_at: add.expired_at,
2224 insertion_order,
2225 },
2226 );
2227 metadata.next_order = Some(insertion_order + 1);
2228 self.certificate_metadata.insert(address, metadata);
2229 }
2230 debug_assert!(
2231 entry.contains_key(&fingerprint),
2232 "add_certificate must insert the fingerprint under its address"
2233 );
2234 debug_assert_eq!(
2235 entry.len(),
2236 before + 1,
2237 "add_certificate inserts exactly one fingerprint on the new path"
2238 );
2239 Ok(())
2240 }
2241
2242 fn remove_certificate(&mut self, remove: &RemoveCertificate) -> Result<(), StateError> {
2243 let fingerprint = Fingerprint(
2244 hex::decode(&remove.fingerprint)
2245 .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
2246 );
2247 let address = remove.address.into();
2248
2249 if self
2250 .certificates
2251 .get(&address)
2252 .is_none_or(|entries| !entries.contains_key(&fingerprint))
2253 {
2254 return Ok(());
2255 }
2256
2257 let mut metadata = self.take_certificate_metadata_for_state_change(address);
2258 Self::remove_certificate_metadata(&mut metadata, &fingerprint);
2259
2260 let became_empty = if let Some(index) = self.certificates.get_mut(&address) {
2261 index.remove(&fingerprint);
2262 debug_assert!(
2263 !index.contains_key(&fingerprint),
2264 "remove_certificate must evict the fingerprint when the address is known"
2265 );
2266 index.is_empty()
2267 } else {
2268 false
2269 };
2270
2271 if became_empty {
2283 self.certificates.remove(&address);
2284 self.certificate_metadata.remove(&address);
2285 } else {
2286 self.certificate_metadata.insert(address, metadata);
2287 }
2288
2289 Ok(())
2290 }
2291
2292 fn replace_certificate(&mut self, replace: &ReplaceCertificate) -> Result<(), StateError> {
2310 let replace_address = replace.address.into();
2311 let old_fingerprint = Fingerprint(
2312 hex::decode(&replace.old_fingerprint)
2313 .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
2314 );
2315
2316 if !self.certificates.contains_key(&replace_address) {
2317 return Err(StateError::NotFound {
2318 kind: ObjectKind::Certificate,
2319 id: replace.address.to_string(),
2320 });
2321 }
2322
2323 let mut new_certificate = replace.new_certificate.to_owned();
2324 new_certificate
2325 .apply_overriding_names()
2326 .map_err(|error| StateError::ReplaceCertificate(error.to_string()))?;
2327 let new_fingerprint = Fingerprint(
2328 calculate_fingerprint(new_certificate.certificate.as_bytes()).map_err(
2329 |fingerprint_err| StateError::ReplaceCertificate(fingerprint_err.to_string()),
2330 )?,
2331 );
2332 let stored_material_matches = self
2333 .certificates
2334 .get(&replace_address)
2335 .and_then(|entries| entries.get(&new_fingerprint))
2336 .is_some_and(|stored| stored == &new_certificate);
2337 let stored_expiration_matches = self
2338 .certificate_metadata
2339 .get(&replace_address)
2340 .and_then(|metadata| metadata.entries.get(&new_fingerprint))
2341 .and_then(|metadata| metadata.expired_at)
2342 == replace.new_expired_at;
2343
2344 if old_fingerprint == new_fingerprint
2348 && stored_material_matches
2349 && stored_expiration_matches
2350 {
2351 return Ok(());
2352 }
2353
2354 let mut metadata = self.take_certificate_metadata_for_state_change(replace_address);
2355 let new_material_already_stored = stored_material_matches
2356 && metadata
2357 .entries
2358 .get(&new_fingerprint)
2359 .is_some_and(|stored| stored.expired_at == replace.new_expired_at);
2360
2361 let certificates =
2362 self.certificates
2363 .get_mut(&replace_address)
2364 .ok_or(StateError::ReplaceCertificate(
2365 "Unlikely error. This entry in the certificate hashmap should be present"
2366 .to_owned(),
2367 ))?;
2368
2369 if !new_material_already_stored {
2370 certificates.remove(&new_fingerprint);
2371 Self::remove_certificate_metadata(&mut metadata, &new_fingerprint);
2372 }
2373 if old_fingerprint != new_fingerprint {
2374 certificates.remove(&old_fingerprint);
2375 Self::remove_certificate_metadata(&mut metadata, &old_fingerprint);
2376 }
2377 if !new_material_already_stored {
2378 let insertion_order = metadata.entries.len() as u64;
2379 certificates.insert(new_fingerprint.clone(), new_certificate);
2380 metadata.entries.insert(
2381 new_fingerprint.clone(),
2382 CertificateMetadata {
2383 expired_at: replace.new_expired_at,
2384 insertion_order,
2385 },
2386 );
2387 metadata.next_order = Some(insertion_order + 1);
2388 }
2389 self.certificate_metadata.insert(replace_address, metadata);
2390
2391 debug_assert!(
2394 self.certificates
2395 .get(&replace_address)
2396 .is_some_and(|certs| certs.contains_key(&new_fingerprint)),
2397 "replace_certificate must leave the new fingerprint present"
2398 );
2399 debug_assert!(
2400 new_fingerprint == old_fingerprint
2401 || self
2402 .certificates
2403 .get(&replace_address)
2404 .is_none_or(|certs| !certs.contains_key(&old_fingerprint)),
2405 "replace_certificate must evict the old fingerprint unless it equals the new one"
2406 );
2407 Ok(())
2408 }
2409
2410 fn add_tcp_frontend(&mut self, front: &RequestTcpFrontend) -> Result<(), StateError> {
2418 let address: SocketAddr = front.address.into();
2419
2420 if self
2423 .clusters
2424 .get(front.cluster_id.as_str())
2425 .is_some_and(|cluster| {
2426 cluster.affinity_header.is_some() || cluster.affinity_cookie.is_some()
2427 })
2428 {
2429 return Err(StateError::InvalidTcpFrontend {
2430 address,
2431 reason: TCP_CLUSTER_AFFINITY_KEY.to_owned(),
2432 });
2433 }
2434
2435 let normalized_sni = match &front.sni {
2443 Some(sni) => Some(validate_sni_pattern(sni).map_err(|config_error| {
2444 StateError::InvalidTcpFrontend {
2445 address,
2446 reason: format!("sni {sni:?} failed SNI shape validation: {config_error}"),
2447 }
2448 })?),
2449 None => None,
2450 };
2451 let alpn = canonical_tcp_alpn(&front.alpn);
2452
2453 if normalized_sni.is_none() && !alpn.is_empty() {
2459 return Err(StateError::InvalidTcpFrontend {
2460 address,
2461 reason: format!(
2462 "alpn = {alpn:?} set without sni: alpn only matches within an SNI-scoped \
2463 preread, so a frontend without sni would silently ignore its alpn list"
2464 ),
2465 });
2466 }
2467
2468 let total_before = self.count_tcp_frontends_raw();
2469
2470 let mut listener_has_no_sni_front = false;
2476 let mut listener_has_sni_front = false;
2477 for existing in self
2478 .tcp_fronts
2479 .values()
2480 .flatten()
2481 .filter(|existing| existing.address == address)
2482 {
2483 if tcp_frontend_matches(existing, address, &normalized_sni, &alpn) {
2484 return Err(StateError::Exists {
2485 kind: ObjectKind::TcpFrontend,
2486 id: format!(
2487 "TcpFrontend {{ address: {address}, sni: {normalized_sni:?}, \
2488 alpn: {alpn:?} }}"
2489 ),
2490 });
2491 }
2492 match &existing.sni {
2493 None => listener_has_no_sni_front = true,
2494 Some(existing_sni) => {
2495 listener_has_sni_front = true;
2496 if normalized_sni.as_deref() != Some(existing_sni.as_str()) {
2503 continue;
2504 }
2505 let existing_is_catch_all = existing.alpn.is_empty();
2506 let new_is_catch_all = alpn.is_empty();
2507 if existing_is_catch_all && new_is_catch_all {
2508 return Err(StateError::InvalidTcpFrontend {
2509 address,
2510 reason: format!(
2511 "sni {existing_sni:?} already has a catch-all (empty alpn) \
2512 frontend: at most one frontend per (address, sni) may omit \
2513 alpn"
2514 ),
2515 });
2516 }
2517 if !existing_is_catch_all
2522 && !new_is_catch_all
2523 && let Some(overlap) = alpn
2524 .iter()
2525 .find(|protocol| existing.alpn.contains(protocol))
2526 {
2527 return Err(StateError::InvalidTcpFrontend {
2528 address,
2529 reason: format!(
2530 "sni {existing_sni:?} already has a frontend matching ALPN \
2531 protocol {overlap:?}: ALPN matchers for the same (address, \
2532 sni) must not overlap"
2533 ),
2534 });
2535 }
2536 }
2537 }
2538 }
2539 match &normalized_sni {
2540 None if listener_has_sni_front => {
2541 return Err(StateError::InvalidTcpFrontend {
2542 address,
2543 reason: "a no-SNI frontend cannot be added to a listener that already has \
2544 SNI-scoped routes"
2545 .to_string(),
2546 });
2547 }
2548 Some(_) if listener_has_no_sni_front => {
2549 return Err(StateError::InvalidTcpFrontend {
2550 address,
2551 reason: "an SNI-scoped frontend cannot be added to a listener that already \
2552 has a no-SNI catch-all cluster"
2553 .to_string(),
2554 });
2555 }
2556 _ => {}
2557 }
2558 debug_assert_eq!(
2562 self.count_tcp_frontends_raw(),
2563 total_before,
2564 "add_tcp_frontend must not mutate tcp_fronts before every admission check has \
2565 passed"
2566 );
2567
2568 let tcp_frontend = TcpFrontend {
2569 cluster_id: front.cluster_id.as_str().into(),
2570 address,
2571 tags: front.tags.clone(),
2572 sni: normalized_sni,
2573 alpn,
2574 };
2575 let tcp_frontends = self
2576 .tcp_fronts
2577 .entry(tcp_frontend.cluster_id.clone())
2578 .or_default();
2579 let before = tcp_frontends.len();
2580 debug_assert_eq!(
2581 *tcp_frontend.cluster_id, *front.cluster_id,
2582 "the built frontend must carry its bucket's cluster_id"
2583 );
2584 tcp_frontends.push(tcp_frontend);
2585 debug_assert_eq!(
2586 tcp_frontends.len(),
2587 before + 1,
2588 "add_tcp_frontend appends exactly one entry"
2589 );
2590 Ok(())
2591 }
2592
2593 fn remove_tcp_frontend(
2594 &mut self,
2595 front_to_remove: &RequestTcpFrontend,
2596 ) -> Result<(), StateError> {
2597 let tcp_frontends =
2598 self.tcp_fronts
2599 .get_mut(front_to_remove.cluster_id.as_str())
2600 .ok_or(StateError::NotFound {
2601 kind: ObjectKind::TcpFrontend,
2602 id: format!(
2603 "RequestTcpFrontend {{ cluster_id: {:?}, address: {:?}, tags: {:?}, sni: {:?}, alpn: {:?} }}",
2604 front_to_remove.cluster_id,
2605 front_to_remove.address,
2606 front_to_remove.tags,
2607 front_to_remove.sni,
2608 front_to_remove.alpn,
2609 ),
2610 })?;
2611
2612 let len = tcp_frontends.len();
2613 let remove_address: SocketAddr = front_to_remove.address.into();
2614 let remove_sni = front_to_remove.sni.as_deref().map(str::to_ascii_lowercase);
2624 let remove_alpn = canonical_tcp_alpn(&front_to_remove.alpn);
2625 tcp_frontends.retain(|front| {
2630 !tcp_frontend_matches(front, remove_address, &remove_sni, &remove_alpn)
2631 });
2632 let after = tcp_frontends.len();
2633 if after == len {
2634 return Err(StateError::NoChange);
2635 }
2636 debug_assert_eq!(
2641 after,
2642 len - 1,
2643 "remove_tcp_frontend drops exactly one entry"
2644 );
2645 debug_assert!(
2646 !tcp_frontends.iter().any(|f| tcp_frontend_matches(
2647 f,
2648 remove_address,
2649 &remove_sni,
2650 &remove_alpn
2651 )),
2652 "remove_tcp_frontend must leave no frontend matching the removed (address, sni, alpn)"
2653 );
2654 Ok(())
2655 }
2656
2657 fn add_udp_frontend(&mut self, front: &RequestUdpFrontend) -> Result<(), StateError> {
2683 let address: SocketAddr = front.address.into();
2684 let udp_frontend = UdpFrontend {
2685 cluster_id: front.cluster_id.as_str().into(),
2686 address,
2687 tags: front.tags.clone(),
2688 };
2689
2690 if self
2691 .backends
2692 .get(front.cluster_id.as_str())
2693 .is_some_and(|backends| backends.iter().any(|b| b.unix_path.is_some()))
2694 {
2695 return Err(StateError::InvalidValue {
2696 field: "unix_path",
2697 reason: UDP_UNIX_BACKEND_REFUSED,
2698 });
2699 }
2700
2701 let total_before = self.count_udp_frontends_raw();
2702
2703 if let Some(current) = self
2708 .udp_fronts
2709 .values()
2710 .flatten()
2711 .find(|existing| existing.address == address)
2712 {
2713 if *current == udp_frontend {
2718 return Err(StateError::Exists {
2719 kind: ObjectKind::UdpFrontend,
2720 id: format!("{udp_frontend:?}"),
2721 });
2722 }
2723 return Err(StateError::UdpFrontendAddressTaken {
2724 address,
2725 cluster_id: udp_frontend.cluster_id.clone(),
2726 current_cluster_id: current.cluster_id.clone(),
2727 current_tags: current.tags.clone(),
2728 });
2729 }
2730
2731 debug_assert_eq!(
2735 self.count_udp_frontends_raw(),
2736 total_before,
2737 "add_udp_frontend must not mutate udp_fronts before every admission check has passed"
2738 );
2739
2740 let udp_frontends = self
2741 .udp_fronts
2742 .entry(udp_frontend.cluster_id.clone())
2743 .or_default();
2744 let before = udp_frontends.len();
2745 udp_frontends.push(udp_frontend);
2746 debug_assert_eq!(
2747 udp_frontends.len(),
2748 before + 1,
2749 "add_udp_frontend appends exactly one entry"
2750 );
2751 debug_assert_eq!(
2755 self.udp_fronts
2756 .values()
2757 .flatten()
2758 .filter(|existing| existing.address == address)
2759 .count(),
2760 1,
2761 "add_udp_frontend must leave exactly one frontend on an address, across all clusters"
2762 );
2763 Ok(())
2764 }
2765
2766 fn remove_udp_frontend(
2767 &mut self,
2768 front_to_remove: &RequestUdpFrontend,
2769 ) -> Result<(), StateError> {
2770 let udp_frontends = self
2771 .udp_fronts
2772 .get_mut(front_to_remove.cluster_id.as_str())
2773 .ok_or(StateError::NotFound {
2774 kind: ObjectKind::UdpFrontend,
2775 id: format!("{front_to_remove:?}"),
2776 })?;
2777
2778 let len = udp_frontends.len();
2779 let remove_address: SocketAddr = front_to_remove.address.into();
2780 let matches_removal = |front: &UdpFrontend| {
2790 front.address == remove_address && front.tags == front_to_remove.tags
2791 };
2792 udp_frontends.retain(|front| !matches_removal(front));
2793 let after = udp_frontends.len();
2794 if after == len {
2795 return Err(StateError::NoChange);
2796 }
2797 debug_assert_eq!(
2802 after,
2803 len - 1,
2804 "remove_udp_frontend drops exactly one entry"
2805 );
2806 debug_assert!(
2807 !udp_frontends.iter().any(matches_removal),
2808 "remove_udp_frontend must leave no frontend matching the removed (address, tags)"
2809 );
2810 Ok(())
2811 }
2812
2813 pub fn health_check_port_unix_conflict(&self, request: &RequestType) -> Option<&'static str> {
2825 let holds_unix_backend = |cluster_id: &str| {
2826 self.backends
2827 .get(cluster_id)
2828 .is_some_and(|backends| backends.iter().any(|b| b.unix_path.is_some()))
2829 };
2830 let port_conflicts = |config: &crate::proto::command::HealthCheckConfig,
2831 cluster_id: &str| {
2832 config.port.is_some()
2833 && crate::config::validate_health_check_config(config).is_ok()
2834 && holds_unix_backend(cluster_id)
2835 };
2836 let conflict = match request {
2837 RequestType::AddCluster(cluster) => {
2838 cluster.udp.is_none()
2839 && cluster
2840 .health_check
2841 .as_ref()
2842 .is_some_and(|config| port_conflicts(config, &cluster.cluster_id))
2843 }
2844 RequestType::SetHealthCheck(set) => {
2845 self.clusters.contains_key(set.cluster_id.as_str())
2846 && port_conflicts(&set.config, &set.cluster_id)
2847 }
2848 RequestType::SetProtectedHealthCheck(set) => {
2849 self.clusters.contains_key(set.cluster_id.as_str())
2850 && port_conflicts(&set.config, &set.cluster_id)
2851 }
2852 RequestType::AddBackend(add) | RequestType::AddUnixBackend(add) => {
2853 add.unix_path.as_deref().is_some_and(|unix_path| {
2854 crate::config::parse_backend_unix_path(unix_path).is_ok()
2855 && !self.is_udp_cluster(&add.cluster_id)
2856 && self
2857 .clusters
2858 .get(add.cluster_id.as_str())
2859 .and_then(|cluster| cluster.health_check.as_ref())
2860 .is_some_and(|config| config.port.is_some())
2861 })
2862 }
2863 _ => false,
2864 };
2865 conflict.then_some(crate::config::HEALTH_CHECK_PORT_UNIX_REFUSED)
2866 }
2867
2868 fn is_udp_cluster(&self, cluster_id: &str) -> bool {
2871 self.udp_fronts
2872 .get(cluster_id)
2873 .is_some_and(|fronts| !fronts.is_empty())
2874 || self
2875 .clusters
2876 .get(cluster_id)
2877 .is_some_and(|cluster| cluster.udp.is_some())
2878 }
2879
2880 fn add_backend(&mut self, add_backend: &AddBackend) -> Result<(), StateError> {
2887 if !self.clusters.contains_key(add_backend.cluster_id.as_str()) {
2888 debug_assert!(
2889 self.backends
2890 .get(add_backend.cluster_id.as_str())
2891 .is_none_or(Vec::is_empty),
2892 "no backend may outlive its cluster"
2893 );
2894 return Err(StateError::ClusterNotFound {
2895 kind: ObjectKind::Backend,
2896 cluster_id: add_backend.cluster_id.to_owned(),
2897 });
2898 }
2899 if let Some(unix_path) = add_backend.unix_path.as_deref() {
2900 crate::config::parse_backend_unix_path(unix_path).map_err(|reason| {
2901 StateError::InvalidValue {
2902 field: "unix_path",
2903 reason,
2904 }
2905 })?;
2906 if self.is_udp_cluster(&add_backend.cluster_id) {
2912 return Err(StateError::InvalidValue {
2913 field: "unix_path",
2914 reason: UDP_UNIX_BACKEND_REFUSED,
2915 });
2916 }
2917 }
2918 let backend = Backend {
2919 address: add_backend.address.into(),
2920 cluster_id: add_backend.cluster_id.as_str().into(),
2921 backend_id: add_backend.backend_id.clone(),
2922 sticky_id: add_backend.sticky_id.clone(),
2923 load_balancing_parameters: add_backend.load_balancing_parameters,
2924 backup: add_backend.backup,
2925 unix_path: add_backend.unix_path.clone(),
2926 };
2927 let backends = self.backends.entry(backend.cluster_id.clone()).or_default();
2928 let backend_id = backend.backend_id.clone();
2929 let backend_address = backend.address;
2930 let before = backends.len();
2931
2932 let was_present = backends
2937 .iter()
2938 .any(|b| b.backend_id == backend_id && b.address == backend_address);
2939 backends.retain(|b| b.backend_id != backend.backend_id || b.address != backend.address);
2940 debug_assert_eq!(
2941 backends.len(),
2942 before - was_present as usize,
2943 "the upsert retain must drop exactly the prior copy iff it existed"
2944 );
2945 backends.push(backend);
2946 backends.sort();
2947
2948 debug_assert_eq!(
2949 backends.len(),
2950 before + (!was_present) as usize,
2951 "add_backend grows the bucket by one iff the backend was new"
2952 );
2953 debug_assert_eq!(
2954 backends
2955 .iter()
2956 .filter(|b| b.backend_id == backend_id && b.address == backend_address)
2957 .count(),
2958 1,
2959 "exactly one copy of the upserted backend must remain"
2960 );
2961 Ok(())
2962 }
2963
2964 fn remove_backend(&mut self, backend: &RemoveBackend) -> Result<(), StateError> {
2965 let backend_list =
2966 self.backends
2967 .get_mut(backend.cluster_id.as_str())
2968 .ok_or(StateError::NotFound {
2969 kind: ObjectKind::Backend,
2970 id: backend.backend_id.to_owned(),
2971 })?;
2972
2973 let len = backend_list.len();
2974 let remove_address: SocketAddr = backend.address.into();
2975 backend_list.retain(|b| b.backend_id != backend.backend_id || b.address != remove_address);
2976 backend_list.sort();
2977 let after = backend_list.len();
2978 if after == len {
2979 return Err(StateError::NoChange);
2980 }
2981 debug_assert_eq!(after, len - 1, "remove_backend drops exactly one entry");
2984 debug_assert!(
2985 !backend_list
2986 .iter()
2987 .any(|b| b.backend_id == backend.backend_id && b.address == remove_address),
2988 "remove_backend must leave no backend matching (backend_id, address)"
2989 );
2990 Ok(())
2991 }
2992
2993 fn generate_requests(&self) -> Vec<Request> {
2995 let mut v: Vec<Request> = Vec::new();
2996
2997 for listener in self.http_listeners.values() {
2998 v.push(RequestType::AddHttpListener(listener.clone()).into());
2999 if listener.active {
3000 v.push(
3001 RequestType::ActivateListener(ActivateListener {
3002 interface: listener.interface.clone(),
3003 address: listener.address,
3004 proxy: ListenerType::Http.into(),
3005 from_scm: false,
3006 })
3007 .into(),
3008 );
3009 }
3010 }
3011
3012 for listener in self.https_listeners.values() {
3013 v.push(RequestType::add_https_listener(listener.clone()).into());
3014 if listener.active {
3015 v.push(
3016 RequestType::ActivateListener(ActivateListener {
3017 interface: listener.interface.clone(),
3018 address: listener.address,
3019 proxy: ListenerType::Https.into(),
3020 from_scm: false,
3021 })
3022 .into(),
3023 );
3024 }
3025 }
3026
3027 for listener in self.tcp_listeners.values() {
3028 v.push(RequestType::AddTcpListener(listener.clone()).into());
3029 if listener.active {
3030 v.push(
3031 RequestType::ActivateListener(ActivateListener {
3032 interface: listener.interface.clone(),
3033 address: listener.address,
3034 proxy: ListenerType::Tcp.into(),
3035 from_scm: false,
3036 })
3037 .into(),
3038 );
3039 }
3040 }
3041
3042 for listener in self.udp_listeners.values() {
3043 v.push(RequestType::AddUdpListener(listener.clone()).into());
3044 if listener.active {
3045 v.push(
3046 RequestType::ActivateListener(ActivateListener {
3047 interface: listener.interface.clone(),
3048 address: listener.address,
3049 proxy: ListenerType::Udp.into(),
3050 from_scm: false,
3051 })
3052 .into(),
3053 );
3054 }
3055 }
3056
3057 for cluster in self.clusters.values() {
3058 v.push(RequestType::AddCluster(cluster.clone()).into());
3059 }
3060
3061 for front in self.http_fronts.values() {
3062 v.push(RequestType::add_http_frontend(front.clone().into()).into());
3063 }
3064
3065 for add in self.certificate_add_requests() {
3066 v.push(RequestType::AddCertificate(add).into());
3067 }
3068
3069 for front in self.https_fronts.values() {
3070 v.push(RequestType::add_https_frontend(front.clone().into()).into());
3071 }
3072
3073 for front_list in self.tcp_fronts.values() {
3074 for front in front_list {
3075 v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
3076 }
3077 }
3078
3079 for front_list in self.udp_fronts.values() {
3080 for front in front_list {
3081 v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
3082 }
3083 }
3084
3085 for backend_list in self.backends.values() {
3086 for backend in backend_list {
3087 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3088 }
3089 }
3090
3091 #[cfg(debug_assertions)]
3097 {
3098 let mut replayed = ConfigState::new();
3099 for request in &v {
3100 debug_assert!(
3101 replayed.dispatch(request).is_ok(),
3102 "every request from generate_requests must replay cleanly"
3103 );
3104 }
3105 debug_assert!(
3106 replayed.clusters == self.clusters
3107 && replayed.backends == self.backends
3108 && replayed.http_listeners == self.http_listeners
3109 && replayed.https_listeners == self.https_listeners
3110 && replayed.tcp_listeners == self.tcp_listeners
3111 && replayed.http_fronts == self.http_fronts
3112 && replayed.https_fronts == self.https_fronts
3113 && replayed.tcp_fronts == self.tcp_fronts
3114 && replayed.certificates == self.certificates
3115 && replayed.certificate_metadata == self.normalized_certificate_metadata(),
3116 "replaying generate_requests into a fresh state must reproduce self"
3117 );
3118 }
3119
3120 v
3121 }
3122
3123 pub fn generate_activate_requests(&self) -> Vec<Request> {
3124 let mut v: Vec<Request> = Vec::new();
3125 for front in self
3126 .http_listeners
3127 .iter()
3128 .filter(|(_, listener)| listener.active)
3129 .map(|(k, _)| k)
3130 {
3131 v.push(
3132 RequestType::ActivateListener(ActivateListener {
3133 interface: front.interface.clone(),
3134 address: SocketAddress::from(front.address),
3135 proxy: ListenerType::Http.into(),
3136 from_scm: false,
3137 })
3138 .into(),
3139 );
3140 }
3141
3142 for front in self
3143 .https_listeners
3144 .iter()
3145 .filter(|(_, listener)| listener.active)
3146 .map(|(k, _)| k)
3147 {
3148 v.push(
3149 RequestType::ActivateListener(ActivateListener {
3150 interface: front.interface.clone(),
3151 address: SocketAddress::from(front.address),
3152 proxy: ListenerType::Https.into(),
3153 from_scm: false,
3154 })
3155 .into(),
3156 );
3157 }
3158 for front in self
3159 .tcp_listeners
3160 .iter()
3161 .filter(|(_, listener)| listener.active)
3162 .map(|(k, _)| k)
3163 {
3164 v.push(
3165 RequestType::ActivateListener(ActivateListener {
3166 interface: front.interface.clone(),
3167 address: SocketAddress::from(front.address),
3168 proxy: ListenerType::Tcp.into(),
3169 from_scm: false,
3170 })
3171 .into(),
3172 );
3173 }
3174 for front in self
3175 .udp_listeners
3176 .iter()
3177 .filter(|(_, listener)| listener.active)
3178 .map(|(k, _)| k)
3179 {
3180 v.push(
3181 RequestType::ActivateListener(ActivateListener {
3182 interface: front.interface.clone(),
3183 address: SocketAddress::from(front.address),
3184 proxy: ListenerType::Udp.into(),
3185 from_scm: false,
3186 })
3187 .into(),
3188 );
3189 }
3190
3191 #[cfg(debug_assertions)]
3194 {
3195 let active_listeners = self.http_listeners.values().filter(|l| l.active).count()
3196 + self.https_listeners.values().filter(|l| l.active).count()
3197 + self.tcp_listeners.values().filter(|l| l.active).count()
3198 + self.udp_listeners.values().filter(|l| l.active).count();
3199 debug_assert_eq!(
3200 v.len(),
3201 active_listeners,
3202 "generate_activate_requests emits one request per active listener"
3203 );
3204 debug_assert!(
3205 v.iter()
3206 .all(|r| matches!(r.request_type, Some(RequestType::ActivateListener(_)))),
3207 "generate_activate_requests must emit only ActivateListener requests"
3208 );
3209 }
3210
3211 v
3212 }
3213
3214 pub fn diff(&self, other: &ConfigState) -> Vec<Request> {
3215 let my_tcp_listeners: HashSet<&ListenerKey> = self.tcp_listeners.keys().collect();
3216 let their_tcp_listeners: HashSet<&ListenerKey> = other.tcp_listeners.keys().collect();
3217 let removed_tcp_listeners = my_tcp_listeners.difference(&their_tcp_listeners);
3218 let added_tcp_listeners = their_tcp_listeners.difference(&my_tcp_listeners);
3219
3220 let my_udp_listeners: HashSet<&ListenerKey> = self.udp_listeners.keys().collect();
3221 let their_udp_listeners: HashSet<&ListenerKey> = other.udp_listeners.keys().collect();
3222 let removed_udp_listeners = my_udp_listeners.difference(&their_udp_listeners);
3223 let added_udp_listeners = their_udp_listeners.difference(&my_udp_listeners);
3224
3225 let my_http_listeners: HashSet<&ListenerKey> = self.http_listeners.keys().collect();
3226 let their_http_listeners: HashSet<&ListenerKey> = other.http_listeners.keys().collect();
3227 let removed_http_listeners = my_http_listeners.difference(&their_http_listeners);
3228 let added_http_listeners = their_http_listeners.difference(&my_http_listeners);
3229
3230 let my_https_listeners: HashSet<&ListenerKey> = self.https_listeners.keys().collect();
3231 let their_https_listeners: HashSet<&ListenerKey> = other.https_listeners.keys().collect();
3232 let removed_https_listeners = my_https_listeners.difference(&their_https_listeners);
3233 let added_https_listeners = their_https_listeners.difference(&my_https_listeners);
3234
3235 let mut v: Vec<Request> = vec![];
3236
3237 for address in removed_tcp_listeners {
3238 if self.tcp_listeners[*address].active {
3239 v.push(
3240 RequestType::DeactivateListener(DeactivateListener {
3241 interface: address.interface.clone(),
3242 address: SocketAddress::from(address.address),
3243 proxy: ListenerType::Tcp.into(),
3244 to_scm: false,
3245 })
3246 .into(),
3247 );
3248 }
3249
3250 v.push(
3251 RequestType::RemoveListener(RemoveListener {
3252 interface: address.interface.clone(),
3253 address: SocketAddress::from(address.address),
3254 proxy: ListenerType::Tcp.into(),
3255 })
3256 .into(),
3257 );
3258 }
3259
3260 for address in added_tcp_listeners.clone() {
3261 v.push(RequestType::AddTcpListener(other.tcp_listeners[*address].clone()).into());
3262
3263 if other.tcp_listeners[*address].active {
3264 v.push(
3265 RequestType::ActivateListener(ActivateListener {
3266 interface: address.interface.clone(),
3267 address: SocketAddress::from(address.address),
3268 proxy: ListenerType::Tcp.into(),
3269 from_scm: false,
3270 })
3271 .into(),
3272 );
3273 }
3274 }
3275
3276 for address in removed_udp_listeners {
3277 if self.udp_listeners[*address].active {
3278 v.push(
3279 RequestType::DeactivateListener(DeactivateListener {
3280 interface: address.interface.clone(),
3281 address: SocketAddress::from(address.address),
3282 proxy: ListenerType::Udp.into(),
3283 to_scm: false,
3284 })
3285 .into(),
3286 );
3287 }
3288
3289 v.push(
3290 RequestType::RemoveListener(RemoveListener {
3291 interface: address.interface.clone(),
3292 address: SocketAddress::from(address.address),
3293 proxy: ListenerType::Udp.into(),
3294 })
3295 .into(),
3296 );
3297 }
3298
3299 for address in added_udp_listeners.clone() {
3300 v.push(RequestType::AddUdpListener(other.udp_listeners[*address].clone()).into());
3301
3302 if other.udp_listeners[*address].active {
3303 v.push(
3304 RequestType::ActivateListener(ActivateListener {
3305 interface: address.interface.clone(),
3306 address: SocketAddress::from(address.address),
3307 proxy: ListenerType::Udp.into(),
3308 from_scm: false,
3309 })
3310 .into(),
3311 );
3312 }
3313 }
3314
3315 for address in removed_http_listeners {
3316 if self.http_listeners[*address].active {
3317 v.push(
3318 RequestType::DeactivateListener(DeactivateListener {
3319 interface: address.interface.clone(),
3320 address: SocketAddress::from(address.address),
3321 proxy: ListenerType::Http.into(),
3322 to_scm: false,
3323 })
3324 .into(),
3325 );
3326 }
3327
3328 v.push(
3329 RequestType::RemoveListener(RemoveListener {
3330 interface: address.interface.clone(),
3331 address: SocketAddress::from(address.address),
3332 proxy: ListenerType::Http.into(),
3333 })
3334 .into(),
3335 );
3336 }
3337
3338 for address in added_http_listeners.clone() {
3339 v.push(RequestType::AddHttpListener(other.http_listeners[*address].clone()).into());
3340
3341 if other.http_listeners[*address].active {
3342 v.push(
3343 RequestType::ActivateListener(ActivateListener {
3344 interface: address.interface.clone(),
3345 address: SocketAddress::from(address.address),
3346 proxy: ListenerType::Http.into(),
3347 from_scm: false,
3348 })
3349 .into(),
3350 );
3351 }
3352 }
3353
3354 for address in removed_https_listeners {
3355 if self.https_listeners[*address].active {
3356 v.push(
3357 RequestType::DeactivateListener(DeactivateListener {
3358 interface: address.interface.clone(),
3359 address: SocketAddress::from(address.address),
3360 proxy: ListenerType::Https.into(),
3361 to_scm: false,
3362 })
3363 .into(),
3364 );
3365 }
3366
3367 v.push(
3368 RequestType::RemoveListener(RemoveListener {
3369 interface: address.interface.clone(),
3370 address: SocketAddress::from(address.address),
3371 proxy: ListenerType::Https.into(),
3372 })
3373 .into(),
3374 );
3375 }
3376
3377 for address in added_https_listeners.clone() {
3378 v.push(RequestType::add_https_listener(other.https_listeners[*address].clone()).into());
3379
3380 if other.https_listeners[*address].active {
3381 v.push(
3382 RequestType::ActivateListener(ActivateListener {
3383 interface: address.interface.clone(),
3384 address: SocketAddress::from(address.address),
3385 proxy: ListenerType::Https.into(),
3386 from_scm: false,
3387 })
3388 .into(),
3389 );
3390 }
3391 }
3392
3393 for addr in my_tcp_listeners.intersection(&their_tcp_listeners) {
3394 let my_listener = &self.tcp_listeners[*addr];
3395 let their_listener = &other.tcp_listeners[*addr];
3396
3397 if my_listener != their_listener {
3398 v.push(
3399 RequestType::RemoveListener(RemoveListener {
3400 interface: addr.interface.clone(),
3401 address: SocketAddress::from(addr.address),
3402 proxy: ListenerType::Tcp.into(),
3403 })
3404 .into(),
3405 );
3406 let mut listener_to_add = their_listener.clone();
3408 listener_to_add.active = false;
3409 v.push(RequestType::AddTcpListener(listener_to_add).into());
3410
3411 if their_listener.active {
3418 v.push(
3419 RequestType::ActivateListener(ActivateListener {
3420 interface: addr.interface.clone(),
3421 address: SocketAddress::from(addr.address),
3422 proxy: ListenerType::Tcp.into(),
3423 from_scm: false,
3424 })
3425 .into(),
3426 );
3427 }
3428 }
3429
3430 if my_listener.active && !their_listener.active {
3431 v.push(
3432 RequestType::DeactivateListener(DeactivateListener {
3433 interface: addr.interface.clone(),
3434 address: SocketAddress::from(addr.address),
3435 proxy: ListenerType::Tcp.into(),
3436 to_scm: false,
3437 })
3438 .into(),
3439 );
3440 }
3441 }
3442
3443 for addr in my_udp_listeners.intersection(&their_udp_listeners) {
3444 let my_listener = &self.udp_listeners[*addr];
3445 let their_listener = &other.udp_listeners[*addr];
3446
3447 if my_listener != their_listener {
3448 v.push(
3449 RequestType::RemoveListener(RemoveListener {
3450 interface: addr.interface.clone(),
3451 address: SocketAddress::from(addr.address),
3452 proxy: ListenerType::Udp.into(),
3453 })
3454 .into(),
3455 );
3456 let mut listener_to_add = their_listener.clone();
3458 listener_to_add.active = false;
3459 v.push(RequestType::AddUdpListener(listener_to_add).into());
3460
3461 if their_listener.active {
3468 v.push(
3469 RequestType::ActivateListener(ActivateListener {
3470 interface: addr.interface.clone(),
3471 address: SocketAddress::from(addr.address),
3472 proxy: ListenerType::Udp.into(),
3473 from_scm: false,
3474 })
3475 .into(),
3476 );
3477 }
3478 }
3479
3480 if my_listener.active && !their_listener.active {
3481 v.push(
3482 RequestType::DeactivateListener(DeactivateListener {
3483 interface: addr.interface.clone(),
3484 address: SocketAddress::from(addr.address),
3485 proxy: ListenerType::Udp.into(),
3486 to_scm: false,
3487 })
3488 .into(),
3489 );
3490 }
3491 }
3492
3493 for addr in my_http_listeners.intersection(&their_http_listeners) {
3494 let my_listener = &self.http_listeners[*addr];
3495 let their_listener = &other.http_listeners[*addr];
3496
3497 if my_listener != their_listener {
3498 v.push(
3499 RequestType::RemoveListener(RemoveListener {
3500 interface: addr.interface.clone(),
3501 address: SocketAddress::from(addr.address),
3502 proxy: ListenerType::Http.into(),
3503 })
3504 .into(),
3505 );
3506 let mut listener_to_add = their_listener.clone();
3508 listener_to_add.active = false;
3509 v.push(RequestType::AddHttpListener(listener_to_add).into());
3510
3511 if their_listener.active {
3518 v.push(
3519 RequestType::ActivateListener(ActivateListener {
3520 interface: addr.interface.clone(),
3521 address: SocketAddress::from(addr.address),
3522 proxy: ListenerType::Http.into(),
3523 from_scm: false,
3524 })
3525 .into(),
3526 );
3527 }
3528 }
3529
3530 if my_listener.active && !their_listener.active {
3531 v.push(
3532 RequestType::DeactivateListener(DeactivateListener {
3533 interface: addr.interface.clone(),
3534 address: SocketAddress::from(addr.address),
3535 proxy: ListenerType::Http.into(),
3536 to_scm: false,
3537 })
3538 .into(),
3539 );
3540 }
3541 }
3542
3543 for addr in my_https_listeners.intersection(&their_https_listeners) {
3544 let my_listener = &self.https_listeners[*addr];
3545 let their_listener = &other.https_listeners[*addr];
3546
3547 if my_listener != their_listener {
3548 v.push(
3549 RequestType::RemoveListener(RemoveListener {
3550 interface: addr.interface.clone(),
3551 address: SocketAddress::from(addr.address),
3552 proxy: ListenerType::Https.into(),
3553 })
3554 .into(),
3555 );
3556 let mut listener_to_add = their_listener.clone();
3558 listener_to_add.active = false;
3559 v.push(RequestType::add_https_listener(listener_to_add).into());
3560
3561 if their_listener.active {
3568 v.push(
3569 RequestType::ActivateListener(ActivateListener {
3570 interface: addr.interface.clone(),
3571 address: SocketAddress::from(addr.address),
3572 proxy: ListenerType::Https.into(),
3573 from_scm: false,
3574 })
3575 .into(),
3576 );
3577 }
3578 }
3579
3580 if my_listener.active && !their_listener.active {
3581 v.push(
3582 RequestType::DeactivateListener(DeactivateListener {
3583 interface: addr.interface.clone(),
3584 address: SocketAddress::from(addr.address),
3585 proxy: ListenerType::Https.into(),
3586 to_scm: false,
3587 })
3588 .into(),
3589 );
3590 }
3591 }
3592
3593 let mut removed_clusters: Vec<&ClusterId> = Vec::new();
3594 let mut deferred_clusters: Vec<Request> = Vec::new();
3599 for (cluster_id, res) in diff_map(self.clusters.iter(), other.clusters.iter()) {
3600 match res {
3601 DiffResult::Added | DiffResult::Changed => {
3602 let add: Request =
3603 RequestType::AddCluster(other.clusters.get(cluster_id).unwrap().clone())
3604 .into();
3605 if add.request_type.as_ref().is_some_and(|request| {
3606 self.health_check_port_unix_conflict(request).is_some()
3607 }) {
3608 deferred_clusters.push(add);
3609 } else {
3610 v.push(add);
3611 }
3612 }
3613 DiffResult::Removed => {
3614 removed_clusters.push(cluster_id);
3615 v.push(RequestType::RemoveCluster(cluster_id.to_string()).into())
3616 }
3617 }
3618 }
3619
3620 let base: Cow<'_, ConfigState> = if removed_clusters.is_empty() {
3627 Cow::Borrowed(self)
3628 } else {
3629 let mut cascaded = self.clone();
3630 for cluster_id in &removed_clusters {
3631 let removed = cascaded.remove_cluster(cluster_id);
3632 debug_assert!(
3633 removed.is_ok(),
3634 "a cluster diff_map reports removed must exist in self"
3635 );
3636 }
3637 Cow::Owned(cascaded)
3638 };
3639
3640 for ((cluster_id, backend_id), res) in diff_map(
3641 base.backends.iter().flat_map(|(cluster_id, v)| {
3642 v.iter()
3643 .map(move |backend| ((cluster_id, &backend.backend_id), backend))
3644 }),
3645 other.backends.iter().flat_map(|(cluster_id, v)| {
3646 v.iter()
3647 .map(move |backend| ((cluster_id, &backend.backend_id), backend))
3648 }),
3649 ) {
3650 match res {
3651 DiffResult::Added => {
3652 let backend = other
3653 .backends
3654 .get(cluster_id)
3655 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3656 .unwrap();
3657 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3658 }
3659 DiffResult::Removed => {
3660 let backend = base
3661 .backends
3662 .get(cluster_id)
3663 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3664 .unwrap();
3665
3666 v.push(
3667 RequestType::RemoveBackend(RemoveBackend {
3668 cluster_id: backend.cluster_id.to_string(),
3669 backend_id: backend.backend_id.clone(),
3670 address: SocketAddress::from(backend.address),
3671 })
3672 .into(),
3673 );
3674 }
3675 DiffResult::Changed => {
3676 let backend = base
3677 .backends
3678 .get(cluster_id)
3679 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3680 .unwrap();
3681
3682 v.push(
3683 RequestType::RemoveBackend(RemoveBackend {
3684 cluster_id: backend.cluster_id.to_string(),
3685 backend_id: backend.backend_id.clone(),
3686 address: SocketAddress::from(backend.address),
3687 })
3688 .into(),
3689 );
3690
3691 let backend = other
3692 .backends
3693 .get(cluster_id)
3694 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3695 .unwrap();
3696 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3697 }
3698 }
3699 }
3700
3701 v.append(&mut deferred_clusters);
3702
3703 let mut my_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
3704 for (route, front) in base.http_fronts.iter() {
3705 my_http_fronts.insert((route, front));
3706 }
3707 let mut their_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
3708 for (route, front) in other.http_fronts.iter() {
3709 their_http_fronts.insert((route, front));
3710 }
3711
3712 let removed_http_fronts = my_http_fronts.difference(&their_http_fronts);
3713 let added_http_fronts = their_http_fronts.difference(&my_http_fronts);
3714
3715 for &(_, front) in removed_http_fronts {
3716 v.push(RequestType::remove_http_frontend(front.clone().into()).into());
3717 }
3718
3719 for &(_, front) in added_http_fronts {
3720 v.push(RequestType::add_http_frontend(front.clone().into()).into());
3721 }
3722
3723 let mut my_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
3724 for (route, front) in base.https_fronts.iter() {
3725 my_https_fronts.insert((route, front));
3726 }
3727 let mut their_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
3728 for (route, front) in other.https_fronts.iter() {
3729 their_https_fronts.insert((route, front));
3730 }
3731 let removed_https_fronts = my_https_fronts.difference(&their_https_fronts);
3732 let added_https_fronts = their_https_fronts.difference(&my_https_fronts);
3733
3734 for &(_, front) in removed_https_fronts {
3735 v.push(RequestType::remove_https_frontend(front.clone().into()).into());
3736 }
3737
3738 for &(_, front) in added_https_fronts {
3739 v.push(RequestType::add_https_frontend(front.clone().into()).into());
3740 }
3741
3742 let mut my_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
3743 for (cluster_id, front_list) in base.tcp_fronts.iter() {
3744 for front in front_list.iter() {
3745 my_tcp_fronts.insert((cluster_id, front));
3746 }
3747 }
3748 let mut their_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
3749 for (cluster_id, front_list) in other.tcp_fronts.iter() {
3750 for front in front_list.iter() {
3751 their_tcp_fronts.insert((cluster_id, front));
3752 }
3753 }
3754
3755 let removed_tcp_fronts = my_tcp_fronts.difference(&their_tcp_fronts);
3756 let added_tcp_fronts = their_tcp_fronts.difference(&my_tcp_fronts);
3757
3758 for &(_, front) in removed_tcp_fronts {
3759 v.push(RequestType::RemoveTcpFrontend(front.clone().into()).into());
3760 }
3761
3762 for &(_, front) in added_tcp_fronts {
3763 v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
3764 }
3765
3766 let mut my_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
3767 for (cluster_id, front_list) in base.udp_fronts.iter() {
3768 for front in front_list.iter() {
3769 my_udp_fronts.insert((cluster_id, front));
3770 }
3771 }
3772 let mut their_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
3773 for (cluster_id, front_list) in other.udp_fronts.iter() {
3774 for front in front_list.iter() {
3775 their_udp_fronts.insert((cluster_id, front));
3776 }
3777 }
3778
3779 let removed_udp_fronts = my_udp_fronts.difference(&their_udp_fronts);
3780 let added_udp_fronts = their_udp_fronts.difference(&my_udp_fronts);
3781
3782 for &(_, front) in removed_udp_fronts {
3783 v.push(RequestType::RemoveUdpFrontend(front.clone().into()).into());
3784 }
3785
3786 for &(_, front) in added_udp_fronts {
3787 v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
3788 }
3789
3790 let mut certificate_shadow = ConfigState {
3797 certificates: self.certificates.to_owned(),
3798 certificate_metadata: self.certificate_metadata.to_owned(),
3799 ..Default::default()
3800 };
3801 let certificate_addresses: BTreeSet<SocketAddr> = self
3802 .certificates
3803 .keys()
3804 .chain(other.certificates.keys())
3805 .copied()
3806 .collect();
3807
3808 for address in certificate_addresses {
3809 let normalized_metadata =
3815 certificate_shadow.normalized_certificate_metadata_for_address(address);
3816 if normalized_metadata.entries.is_empty() {
3817 certificate_shadow.certificate_metadata.remove(&address);
3818 } else {
3819 certificate_shadow
3820 .certificate_metadata
3821 .insert(address, normalized_metadata);
3822 }
3823 let target: Vec<(Fingerprint, AddCertificate)> = other
3824 .ordered_certificate_entries(address)
3825 .into_iter()
3826 .map(|(fingerprint, certificate, metadata)| {
3827 (
3828 fingerprint,
3829 AddCertificate {
3830 address: SocketAddress::from(address),
3831 certificate: certificate.to_owned(),
3832 expired_at: metadata.expired_at,
3833 },
3834 )
3835 })
3836 .collect();
3837 let target_fingerprints: BTreeSet<Fingerprint> = target
3838 .iter()
3839 .map(|(fingerprint, _)| fingerprint.to_owned())
3840 .collect();
3841 let current_fingerprints: BTreeSet<Fingerprint> = certificate_shadow
3842 .certificates
3843 .get(&address)
3844 .into_iter()
3845 .flat_map(HashMap::keys)
3846 .map(ToOwned::to_owned)
3847 .collect();
3848
3849 for fingerprint in current_fingerprints.difference(&target_fingerprints) {
3850 let request: Request = RequestType::RemoveCertificate(RemoveCertificate {
3851 address: SocketAddress::from(address),
3852 fingerprint: fingerprint.to_string(),
3853 })
3854 .into();
3855 let result = certificate_shadow.dispatch(&request);
3856 debug_assert!(
3857 result.is_ok(),
3858 "certificate removal diff must replay: {result:?}"
3859 );
3860 v.push(request);
3861 }
3862
3863 for (fingerprint, add) in &target {
3864 let current_certificate = certificate_shadow
3865 .certificates
3866 .get(&address)
3867 .and_then(|entries| entries.get(fingerprint));
3868 let current_expired_at = certificate_shadow
3869 .certificate_metadata
3870 .get(&address)
3871 .and_then(|index| index.entries.get(fingerprint))
3872 .and_then(|metadata| metadata.expired_at);
3873
3874 let request_type = match current_certificate {
3875 None => None,
3880 Some(current)
3881 if current != &add.certificate || current_expired_at != add.expired_at =>
3882 {
3883 Some(RequestType::ReplaceCertificate(ReplaceCertificate {
3884 address: SocketAddress::from(address),
3885 new_certificate: add.certificate.to_owned(),
3886 old_fingerprint: fingerprint.to_string(),
3887 new_expired_at: add.expired_at,
3888 }))
3889 }
3890 Some(_) => None,
3891 };
3892
3893 if let Some(request_type) = request_type {
3894 let request: Request = request_type.into();
3895 let result = certificate_shadow.dispatch(&request);
3896 debug_assert!(
3897 result.is_ok(),
3898 "certificate update diff must replay: {result:?}"
3899 );
3900 v.push(request);
3901 }
3902 }
3903
3904 let target_order: Vec<Fingerprint> = target
3905 .iter()
3906 .map(|(fingerprint, _)| fingerprint.to_owned())
3907 .collect();
3908 let shadow_order: Vec<Fingerprint> = certificate_shadow
3909 .ordered_certificate_entries(address)
3910 .into_iter()
3911 .map(|(fingerprint, _, _)| fingerprint)
3912 .collect();
3913 if shadow_order != target_order {
3914 let mut shadow_cursor = 0usize;
3926 let mut retained_prefix_len = 0usize;
3927 for fingerprint in &target_order {
3928 let Some(offset) = shadow_order[shadow_cursor..]
3929 .iter()
3930 .position(|candidate| candidate == fingerprint)
3931 else {
3932 break;
3933 };
3934 shadow_cursor += offset + 1;
3935 retained_prefix_len += 1;
3936 }
3937
3938 for (fingerprint, add) in target.iter().skip(retained_prefix_len) {
3939 let already_present = certificate_shadow
3940 .certificates
3941 .get(&address)
3942 .is_some_and(|certificates| certificates.contains_key(fingerprint));
3943 let mut request_types = Vec::with_capacity(usize::from(already_present) + 1);
3944 if already_present {
3945 request_types.push(RequestType::RemoveCertificate(RemoveCertificate {
3946 address: SocketAddress::from(address),
3947 fingerprint: fingerprint.to_string(),
3948 }));
3949 }
3950 request_types.push(RequestType::AddCertificate(add.to_owned()));
3951 for request_type in request_types {
3952 let request: Request = request_type.into();
3953 let result = certificate_shadow.dispatch(&request);
3954 debug_assert!(
3955 result.is_ok(),
3956 "certificate order diff must replay: {result:?}"
3957 );
3958 v.push(request);
3959 }
3960 }
3961 }
3962
3963 debug_assert_eq!(
3964 certificate_shadow.certificate_add_requests_for_address(address),
3965 other.certificate_add_requests_for_address(address),
3966 "certificate diff must converge payload, expiration and insertion order"
3967 );
3968 }
3969
3970 for address in added_tcp_listeners {
3971 let listener = &other.tcp_listeners[*address];
3972 if listener.active {
3973 v.push(
3974 RequestType::ActivateListener(ActivateListener {
3975 interface: listener.interface.clone(),
3976 address: listener.address,
3977 proxy: ListenerType::Tcp.into(),
3978 from_scm: false,
3979 })
3980 .into(),
3981 );
3982 }
3983 }
3984
3985 for address in added_udp_listeners {
3986 let listener = &other.udp_listeners[*address];
3987 if listener.active {
3988 v.push(
3989 RequestType::ActivateListener(ActivateListener {
3990 interface: listener.interface.clone(),
3991 address: listener.address,
3992 proxy: ListenerType::Udp.into(),
3993 from_scm: false,
3994 })
3995 .into(),
3996 );
3997 }
3998 }
3999
4000 #[cfg(debug_assertions)]
4013 {
4014 let mut replayed = self.clone();
4015 for request in &v {
4016 debug_assert!(
4018 replayed.dispatch(request).is_ok(),
4019 "every request emitted by diff must replay cleanly onto self"
4020 );
4021 }
4022 let nonempty = |m: &BTreeMap<ClusterId, Vec<Backend>>| {
4023 m.iter()
4024 .filter(|(_, v)| !v.is_empty())
4025 .map(|(k, v)| (k.clone(), v.clone()))
4026 .collect::<BTreeMap<_, _>>()
4027 };
4028 let nonempty_tcp = |m: &HashMap<ClusterId, Vec<TcpFrontend>>| {
4033 m.iter()
4034 .filter(|(_, v)| !v.is_empty())
4035 .map(|(k, v)| {
4036 (
4037 k.clone(),
4038 v.iter().cloned().collect::<BTreeSet<TcpFrontend>>(),
4039 )
4040 })
4041 .collect::<HashMap<_, _>>()
4042 };
4043 debug_assert!(
4044 replayed.clusters == other.clusters
4045 && nonempty(&replayed.backends) == nonempty(&other.backends)
4046 && replayed.http_fronts == other.http_fronts
4047 && replayed.https_fronts == other.https_fronts
4048 && nonempty_tcp(&replayed.tcp_fronts) == nonempty_tcp(&other.tcp_fronts)
4049 && replayed.certificates == other.certificates
4050 && replayed.normalized_certificate_metadata()
4054 == other.normalized_certificate_metadata()
4055 && replayed.http_listeners == other.http_listeners
4056 && replayed.https_listeners == other.https_listeners
4057 && replayed.tcp_listeners == other.tcp_listeners
4058 && replayed.udp_listeners == other.udp_listeners,
4059 "replaying diff(self, other) onto self must reproduce other's clusters/backends/frontends/certificates/listeners"
4060 );
4061 }
4062
4063 v
4064 }
4065
4066 pub fn hash_state(&self) -> BTreeMap<ClusterId, u64> {
4068 let mut hm: HashMap<ClusterId, DefaultHasher> = self
4069 .clusters
4070 .keys()
4071 .map(|cluster_id| {
4072 let mut hasher = DefaultHasher::new();
4073 self.clusters.get(cluster_id).hash(&mut hasher);
4074 if let Some(backends) = self.backends.get(cluster_id) {
4075 backends.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4076 }
4077 if let Some(tcp_fronts) = self.tcp_fronts.get(cluster_id) {
4078 tcp_fronts.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4079 }
4080 (cluster_id.to_owned(), hasher)
4081 })
4082 .collect();
4083
4084 for front in self.http_fronts.values() {
4085 if let Some(cluster_id) = &front.cluster_id
4086 && let Some(hasher) = hm.get_mut(cluster_id)
4087 {
4088 front.hash(hasher);
4089 }
4090 }
4091
4092 for front in self.https_fronts.values() {
4093 if let Some(cluster_id) = &front.cluster_id
4094 && let Some(hasher) = hm.get_mut(cluster_id)
4095 {
4096 front.hash(hasher);
4097 }
4098 }
4099
4100 hm.drain()
4101 .map(|(cluster_id, hasher)| (cluster_id, hasher.finish()))
4102 .collect()
4103 }
4104
4105 pub fn cluster_state(&self, cluster_id: &str) -> Option<ClusterInformation> {
4108 let configuration = self.clusters.get(cluster_id).cloned()?;
4109 info!("{:?}", configuration);
4110
4111 let http_frontends: Vec<RequestHttpFrontend> = self
4112 .http_fronts
4113 .values()
4114 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4115 .map(|front| front.clone().into())
4116 .collect();
4117
4118 let https_frontends: Vec<RequestHttpFrontend> = self
4119 .https_fronts
4120 .values()
4121 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4122 .map(|front| front.clone().into())
4123 .collect();
4124
4125 let tcp_frontends: Vec<RequestTcpFrontend> = self
4126 .tcp_fronts
4127 .get(cluster_id)
4128 .cloned()
4129 .unwrap_or_default()
4130 .iter()
4131 .map(|front| front.clone().into())
4132 .collect();
4133
4134 let udp_frontends: Vec<RequestUdpFrontend> = self
4135 .udp_fronts
4136 .get(cluster_id)
4137 .cloned()
4138 .unwrap_or_default()
4139 .iter()
4140 .map(|front| front.clone().into())
4141 .collect();
4142
4143 let backends: Vec<AddBackend> = self
4144 .backends
4145 .get(cluster_id)
4146 .cloned()
4147 .unwrap_or_default()
4148 .iter()
4149 .map(|backend| backend.clone().into())
4150 .collect();
4151
4152 Some(ClusterInformation {
4153 configuration: Some(configuration),
4154 http_frontends,
4155 https_frontends,
4156 tcp_frontends,
4157 backends,
4158 udp_frontends,
4159 })
4160 }
4161
4162 pub fn count_backends(&self) -> usize {
4163 self.backends.values().fold(0, |acc, v| acc + v.len())
4164 }
4165
4166 pub fn count_frontends(&self) -> usize {
4167 self.http_fronts.values().count()
4168 + self.https_fronts.values().count()
4169 + self.tcp_fronts.values().fold(0, |acc, v| acc + v.len())
4170 + self.udp_fronts.values().fold(0, |acc, v| acc + v.len())
4171 }
4172
4173 pub fn get_cluster_ids_by_domain(
4174 &self,
4175 hostname: String,
4176 path: Option<String>,
4177 ) -> HashSet<ClusterId> {
4178 let mut cluster_ids: HashSet<ClusterId> = HashSet::new();
4179
4180 self.http_fronts.values().for_each(|front| {
4181 if domain_check(&front.hostname, &front.path, &hostname, &path)
4182 && let Some(id) = &front.cluster_id
4183 {
4184 cluster_ids.insert(id.clone());
4185 }
4186 });
4187
4188 self.https_fronts.values().for_each(|front| {
4189 if domain_check(&front.hostname, &front.path, &hostname, &path)
4190 && let Some(id) = &front.cluster_id
4191 {
4192 cluster_ids.insert(id.clone());
4193 }
4194 });
4195
4196 cluster_ids
4197 }
4198
4199 pub fn get_certificates(
4217 &self,
4218 filters: QueryCertificatesFilters,
4219 ) -> BTreeMap<String, CertificateAndKey> {
4220 self.certificates
4221 .values()
4222 .flat_map(|hash_map| hash_map.iter())
4223 .filter(|(fingerprint, cert)| {
4224 if let Some(domain) = &filters.domain {
4225 cert.names.contains(domain)
4226 } else if let Some(f) = &filters.fingerprint {
4227 fingerprint.to_string() == *f
4228 } else {
4229 true
4230 }
4231 })
4232 .map(|(fingerprint, cert)| (fingerprint.to_string(), cert.to_owned()))
4233 .collect()
4234 }
4235
4236 pub fn list_frontends(&self, filters: FrontendFilters) -> ListedFrontends {
4237 let list_all = !filters.http && !filters.https && !filters.tcp;
4239
4240 let matches_cluster = |cluster_id: Option<&str>| match &filters.cluster_id {
4243 Some(wanted) => cluster_id == Some(wanted.as_str()),
4244 None => true,
4245 };
4246 let matches_domain = |hostname: &str| match &filters.domain {
4247 Some(domain) => hostname.contains(domain),
4248 None => true,
4249 };
4250
4251 let mut listed_frontends = ListedFrontends::default();
4252
4253 if filters.http || list_all {
4254 for http_frontend in self.http_fronts.iter().filter(|f| {
4255 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4256 }) {
4257 listed_frontends
4258 .http_frontends
4259 .push(http_frontend.1.to_owned().into());
4260 }
4261 }
4262
4263 if filters.https || list_all {
4264 for https_frontend in self.https_fronts.iter().filter(|f| {
4265 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4266 }) {
4267 listed_frontends
4268 .https_frontends
4269 .push(https_frontend.1.to_owned().into());
4270 }
4271 }
4272
4273 if (filters.tcp || list_all) && filters.domain.is_none() {
4274 for tcp_frontend in fronts_of_cluster(&self.tcp_fronts, filters.cluster_id.as_deref()) {
4275 listed_frontends
4276 .tcp_frontends
4277 .push(tcp_frontend.to_owned().into())
4278 }
4279 }
4280
4281 if (filters.tcp || list_all) && filters.domain.is_none() {
4287 for udp_frontend in fronts_of_cluster(&self.udp_fronts, filters.cluster_id.as_deref()) {
4288 listed_frontends
4289 .udp_frontends
4290 .push(udp_frontend.to_owned().into())
4291 }
4292 }
4293
4294 listed_frontends
4295 }
4296
4297 pub fn list_listeners(&self) -> ListenersList {
4298 ListenersList {
4299 http_listeners: self
4300 .http_listeners
4301 .iter()
4302 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4303 .collect(),
4304 https_listeners: self
4305 .https_listeners
4306 .iter()
4307 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4308 .collect(),
4309 tcp_listeners: self
4310 .tcp_listeners
4311 .iter()
4312 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4313 .collect(),
4314 udp_listeners: self
4315 .udp_listeners
4316 .iter()
4317 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4318 .collect(),
4319 }
4320 }
4321
4322 pub fn produce_initial_state(&self) -> InitialState {
4324 let mut worker_requests = Vec::new();
4325 for (counter, request) in self.generate_requests().into_iter().enumerate() {
4326 worker_requests.push(WorkerRequest::new(format!("SAVE-{counter}"), request));
4327 }
4328 InitialState {
4329 requests: worker_requests,
4330 }
4331 }
4332
4333 pub fn write_initial_state_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4336 let initial_state = self.produce_initial_state();
4337 let count = initial_state.requests.len();
4338
4339 let bytes_to_write = initial_state.encode_to_vec();
4340 println!("writing {} in the temp file", bytes_to_write.len());
4341 file.write_all(&bytes_to_write)
4342 .map_err(StateError::FileError)?;
4343
4344 file.sync_all().map_err(StateError::FileError)?;
4345
4346 Ok(count)
4347 }
4348
4349 pub fn write_requests_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4353 let mut counter = 0usize;
4354 let requests = self.generate_requests();
4355
4356 for request in requests {
4357 let message = WorkerRequest::new(format!("SAVE-{counter}"), request);
4358
4359 file.write_all(
4360 &serde_json::to_string(&message)
4361 .map(|s| s.into_bytes())
4362 .unwrap_or_default(),
4363 )
4364 .map_err(StateError::FileError)?;
4365
4366 file.write_all(&b"\n\0"[..])
4367 .map_err(StateError::FileError)?;
4368
4369 if counter.is_multiple_of(1000) {
4370 info!("writing {} commands to file", counter);
4371 file.sync_all().map_err(StateError::FileError)?;
4372 }
4373 counter += 1;
4374 }
4375 file.sync_all().map_err(StateError::FileError)?;
4376
4377 Ok(counter)
4378 }
4379}
4380
4381macro_rules! require_h2_knob_floor {
4389 ($config:ident, $field:ident, $minimum:literal) => {
4390 if let Some(value) = $config.$field
4391 && u64::from(value) < $minimum
4392 {
4393 return Err(StateError::InvalidValue {
4394 field: stringify!($field),
4395 reason: concat!("must be >= ", stringify!($minimum)),
4396 });
4397 }
4398 };
4399}
4400
4401pub fn validate_h2_flood_knobs_http(patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
4419 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4420 Ok(())
4421}
4422
4423pub fn validate_h2_flood_knobs_https(patch: &UpdateHttpsListenerConfig) -> Result<(), StateError> {
4425 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4426 Ok(())
4427}
4428
4429pub fn validate_h2_flood_knobs_http_listener(
4453 config: &HttpListenerConfig,
4454) -> Result<(), StateError> {
4455 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4456 Ok(())
4457}
4458
4459pub fn validate_h2_flood_knobs_https_listener(
4463 config: &HttpsListenerConfig,
4464) -> Result<(), StateError> {
4465 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4466 Ok(())
4467}
4468
4469fn merge_answers(stored: &mut BTreeMap<String, String>, patch: &BTreeMap<String, String>) {
4479 for (code, body) in patch {
4480 if !body.is_empty() {
4481 stored.insert(code.to_owned(), body.to_owned());
4482 }
4483 }
4484}
4485
4486pub fn merge_custom_http_answers(
4494 target: &mut Option<CustomHttpAnswers>,
4495 patch: &CustomHttpAnswers,
4496) {
4497 let current = target.get_or_insert_with(CustomHttpAnswers::default);
4498 macro_rules! merge_field {
4499 ($field:ident) => {
4500 if let Some(ref v) = patch.$field {
4501 current.$field = Some(v.clone());
4502 }
4503 };
4504 }
4505 merge_field!(answer_301);
4506 merge_field!(answer_400);
4507 merge_field!(answer_401);
4508 merge_field!(answer_404);
4509 merge_field!(answer_408);
4510 merge_field!(answer_413);
4511 merge_field!(answer_421);
4512 merge_field!(answer_502);
4513 merge_field!(answer_503);
4514 merge_field!(answer_504);
4515 merge_field!(answer_507);
4516}
4517
4518pub fn validate_alpn_protocols(values: &[String]) -> Result<(), StateError> {
4521 for value in values {
4522 if value != "h2" && value != "http/1.1" {
4523 return Err(StateError::InvalidValue {
4524 field: "alpn_protocols",
4525 reason: "each value must be \"h2\" or \"http/1.1\"",
4526 });
4527 }
4528 }
4529 Ok(())
4530}
4531
4532pub fn validate_sozu_id_header(value: &str) -> Result<(), StateError> {
4543 if value.is_empty() {
4544 return Err(StateError::InvalidValue {
4545 field: "sozu_id_header",
4546 reason: "must not be empty",
4547 });
4548 }
4549 for b in value.bytes() {
4550 let is_tchar = b.is_ascii_alphanumeric()
4551 || matches!(
4552 b,
4553 b'!' | b'#'
4554 | b'$'
4555 | b'%'
4556 | b'&'
4557 | b'\''
4558 | b'*'
4559 | b'+'
4560 | b'-'
4561 | b'.'
4562 | b'^'
4563 | b'_'
4564 | b'`'
4565 | b'|'
4566 | b'~'
4567 );
4568 if !is_tchar {
4569 return Err(StateError::InvalidValue {
4570 field: "sozu_id_header",
4571 reason: "must be a valid HTTP header name (RFC 9110 §5.1 token: alphanumeric or one of !#$%&'*+-.^_`|~)",
4572 });
4573 }
4574 }
4575 Ok(())
4576}
4577
4578pub fn validate_hsts_patch(hsts: &HstsConfig) -> Result<(), StateError> {
4583 if hsts.enabled.is_none() {
4584 return Err(StateError::InvalidValue {
4585 field: "hsts",
4586 reason: "`enabled` is required whenever the hsts block is present",
4587 });
4588 }
4589 Ok(())
4590}
4591
4592pub fn validate_forwarded_headers(value: i32) -> Result<(), StateError> {
4597 ForwardedHeaders::try_from(value)
4598 .map(|_| ())
4599 .map_err(|_| StateError::InvalidValue {
4600 field: "forwarded_headers",
4601 reason: "must be one of both, x_forwarded, rfc7239, none",
4602 })
4603}
4604
4605fn fronts_of_cluster<'a, F>(
4609 fronts: &'a HashMap<ClusterId, Vec<F>>,
4610 cluster_id: Option<&str>,
4611) -> impl Iterator<Item = &'a F> {
4612 let (one, all) = match cluster_id {
4613 Some(cluster_id) => (fronts.get(cluster_id), None),
4614 None => (None, Some(fronts.values())),
4615 };
4616 one.into_iter().chain(all.into_iter().flatten()).flatten()
4617}
4618
4619fn domain_check(
4620 front_hostname: &str,
4621 front_path_rule: &PathRule,
4622 hostname: &str,
4623 path_prefix: &Option<String>,
4624) -> bool {
4625 if hostname != front_hostname {
4626 return false;
4627 }
4628
4629 if let Some(path) = &path_prefix {
4630 return path == &front_path_rule.value;
4631 }
4632
4633 true
4634}
4635
4636fn canonical_tcp_alpn(alpn: &[String]) -> Vec<String> {
4646 let mut alpn = alpn.to_vec();
4647 alpn.sort();
4648 alpn.dedup();
4649 debug_assert!(
4653 alpn.windows(2).all(|pair| pair[0] < pair[1]),
4654 "canonical_tcp_alpn must return a strictly sorted, duplicate-free list"
4655 );
4656 alpn
4657}
4658
4659fn tcp_frontend_matches(
4670 front: &TcpFrontend,
4671 address: SocketAddr,
4672 sni: &Option<String>,
4673 alpn: &[String],
4674) -> bool {
4675 front.address == address && front.sni == *sni && front.alpn.as_slice() == alpn
4676}
4677
4678struct DiffMap<'a, K: Ord, V, I1, I2> {
4679 my_it: I1,
4680 other_it: I2,
4681 my: Option<(K, &'a V)>,
4682 other: Option<(K, &'a V)>,
4683}
4684
4685fn diff_map<
4687 'a,
4688 K: Ord,
4689 V: PartialEq,
4690 I1: Iterator<Item = (K, &'a V)>,
4691 I2: Iterator<Item = (K, &'a V)>,
4692>(
4693 my: I1,
4694 other: I2,
4695) -> DiffMap<'a, K, V, I1, I2> {
4696 DiffMap {
4697 my_it: my,
4698 other_it: other,
4699 my: None,
4700 other: None,
4701 }
4702}
4703
4704enum DiffResult {
4705 Added,
4706 Removed,
4707 Changed,
4708}
4709
4710impl<'a, K: Ord, V: PartialEq, I1: Iterator<Item = (K, &'a V)>, I2: Iterator<Item = (K, &'a V)>>
4713 std::iter::Iterator for DiffMap<'a, K, V, I1, I2>
4714{
4715 type Item = (K, DiffResult);
4716
4717 fn next(&mut self) -> Option<Self::Item> {
4718 loop {
4719 if self.my.is_none() {
4720 self.my = self.my_it.next();
4721 }
4722 if self.other.is_none() {
4723 self.other = self.other_it.next();
4724 }
4725
4726 match (self.my.take(), self.other.take()) {
4727 (None, other) => return other.map(|(k, _)| (k, DiffResult::Added)),
4731 (Some((k, _)), None) => return Some((k, DiffResult::Removed)),
4734 (Some((k1, _v1)), Some((k2, v2))) if k1 < k2 => {
4736 self.other = Some((k2, v2));
4737 return Some((k1, DiffResult::Removed));
4738 }
4739 (Some((k1, v1)), Some((k2, _v2))) if k1 > k2 => {
4741 self.my = Some((k1, v1));
4742 return Some((k2, DiffResult::Added));
4743 }
4744 (Some((k1, v1)), Some((_k2, v2))) if v1 != v2 => {
4745 return Some((k1, DiffResult::Changed));
4748 }
4749 _ => {}
4750 }
4751 }
4752 }
4753}
4754
4755#[cfg(test)]
4756mod tests {
4757 use rand::{RngExt, rng, seq::SliceRandom};
4758
4759 use super::*;
4760 use crate::proto::command::{
4761 ClientAuthMode, CloseSessions, CustomHttpAnswers, Header, HeaderPosition, HstsConfig,
4762 LoadBalancingParams, PathRuleKind, RedirectPolicy, RedirectScheme, RequestHttpFrontend,
4763 RequestTcpFrontend, RequestUdpFrontend, RulePosition, UdpListenerConfig,
4764 UpdateUdpListenerConfig,
4765 };
4766
4767 fn capture_test_logs(run: impl FnOnce() + Send + 'static) -> String {
4768 let receiver = std::net::UdpSocket::bind("127.0.0.1:0")
4769 .expect("test log receiver must bind to a loopback port");
4770 let target = format!(
4771 "udp://{}",
4772 receiver
4773 .local_addr()
4774 .expect("test log receiver must have a local address")
4775 );
4776
4777 std::thread::spawn(move || {
4778 crate::logging::Logger::init(
4779 "state-log-redaction-test".to_owned(),
4780 "info",
4781 &target,
4782 false,
4783 None,
4784 None,
4785 None,
4786 )
4787 .expect("test logger must initialize");
4788 run();
4789 })
4790 .join()
4791 .expect("log-producing test thread must not panic");
4792
4793 receiver
4794 .set_nonblocking(true)
4795 .expect("test log receiver must become nonblocking");
4796 let mut output = String::new();
4797 let mut datagram = vec![0; 65_507];
4798 loop {
4799 match receiver.recv(&mut datagram) {
4800 Ok(length) => output.push_str(&String::from_utf8_lossy(&datagram[..length])),
4801 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
4802 Err(error) => panic!("test log receiver failed: {error}"),
4803 }
4804 }
4805 assert!(!output.is_empty(), "test log capture received no datagrams");
4806 output
4807 }
4808
4809 #[test]
4810 fn duplicate_certificate_log_redacts_names() {
4811 const NAME_SECRET: &str = "DUPLICATE_CERTIFICATE_NAME_SECRET_SENTINEL";
4812
4813 let name = format!("{NAME_SECRET}{}", "x".repeat(4096));
4814 let name_len = name.len();
4815 let certificate = CertificateAndKey {
4816 certificate: include_str!("../assets/certificate.pem").to_owned(),
4817 key: include_str!("../assets/key.pem").to_owned(),
4818 certificate_chain: Vec::new(),
4819 versions: Vec::new(),
4820 names: vec![name],
4821 };
4822 let fingerprint = certificate
4823 .fingerprint()
4824 .expect("test certificate fingerprint must be computable")
4825 .to_string();
4826 let output = capture_test_logs(move || {
4827 let mut state = ConfigState::default();
4828 let add = AddCertificate {
4829 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
4830 certificate,
4831 expired_at: None,
4832 };
4833
4834 state
4835 .add_certificate(&add)
4836 .expect("first certificate insertion must succeed");
4837 state
4838 .add_certificate(&add)
4839 .expect("duplicate certificate insertion must be skipped");
4840 });
4841
4842 assert!(
4843 !output.contains(NAME_SECRET),
4844 "duplicate certificate log leaked certificate name {NAME_SECRET}"
4845 );
4846 assert!(
4847 !output.contains(&fingerprint),
4848 "duplicate certificate log leaked certificate fingerprint {fingerprint}"
4849 );
4850 for metadata in [
4851 "fingerprint_bytes=32".to_owned(),
4852 "names_count=1".to_owned(),
4853 format!("names_bytes={name_len}"),
4854 ] {
4855 assert!(
4856 output.contains(&metadata),
4857 "duplicate certificate log omitted bounded metadata {metadata}: {output}"
4858 );
4859 }
4860 assert!(
4861 output.len() <= 512,
4862 "duplicate certificate log is not bounded: {} bytes",
4863 output.len()
4864 );
4865 }
4866
4867 #[test]
4878 fn replace_certificate_with_malformed_pem_keeps_old_certificate() {
4879 let certificate = CertificateAndKey {
4880 certificate: include_str!("../assets/certificate.pem").to_owned(),
4881 key: include_str!("../assets/key.pem").to_owned(),
4882 certificate_chain: Vec::new(),
4883 versions: Vec::new(),
4884 names: Vec::new(),
4885 };
4886 let old_fingerprint = certificate
4887 .fingerprint()
4888 .expect("test certificate fingerprint must be computable");
4889 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4890
4891 let mut state = ConfigState::default();
4892 state
4893 .add_certificate(&AddCertificate {
4894 address,
4895 certificate,
4896 expired_at: None,
4897 })
4898 .expect("initial certificate insertion must succeed");
4899
4900 let malformed = CertificateAndKey {
4901 certificate: "not a valid pem certificate".to_owned(),
4902 key: include_str!("../assets/key.pem").to_owned(),
4903 certificate_chain: Vec::new(),
4904 versions: Vec::new(),
4905 names: Vec::new(),
4906 };
4907 let error = state
4908 .replace_certificate(&ReplaceCertificate {
4909 address,
4910 new_certificate: malformed,
4911 old_fingerprint: old_fingerprint.to_string(),
4912 new_expired_at: None,
4913 })
4914 .expect_err("replacing with a malformed PEM must fail");
4915 assert!(
4916 matches!(error, StateError::ReplaceCertificate(_)),
4917 "expected StateError::ReplaceCertificate, got {error:?}"
4918 );
4919
4920 let certificates_for_address = state
4921 .certificates
4922 .get(&SocketAddr::from(address))
4923 .expect("the address must still be present in the certificate map");
4924 assert!(
4925 certificates_for_address.contains_key(&old_fingerprint),
4926 "a failed replace must leave the original certificate in place, found {:?}",
4927 certificates_for_address.keys().collect::<Vec<_>>()
4928 );
4929 assert_eq!(
4930 certificates_for_address.len(),
4931 1,
4932 "a failed replace must not leave the address with zero (or extra) certificates"
4933 );
4934 }
4935
4936 #[test]
4943 fn replace_certificate_with_same_fingerprint_keeps_certificate() {
4944 let certificate = CertificateAndKey {
4945 certificate: include_str!("../assets/certificate.pem").to_owned(),
4946 key: include_str!("../assets/key.pem").to_owned(),
4947 certificate_chain: Vec::new(),
4948 versions: Vec::new(),
4949 names: Vec::new(),
4950 };
4951 let fingerprint = certificate
4952 .fingerprint()
4953 .expect("test certificate fingerprint must be computable");
4954 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4955
4956 let mut state = ConfigState::default();
4957 state
4958 .add_certificate(&AddCertificate {
4959 address,
4960 certificate: certificate.clone(),
4961 expired_at: None,
4962 })
4963 .expect("initial certificate insertion must succeed");
4964
4965 state
4966 .replace_certificate(&ReplaceCertificate {
4967 address,
4968 new_certificate: certificate,
4969 old_fingerprint: fingerprint.to_string(),
4970 new_expired_at: None,
4971 })
4972 .expect("idempotent replace with the identical certificate must succeed");
4973
4974 let certificates_for_address = state
4975 .certificates
4976 .get(&SocketAddr::from(address))
4977 .expect("the address must still be present in the certificate map");
4978 assert!(
4979 certificates_for_address.contains_key(&fingerprint),
4980 "an idempotent replace (old fingerprint == new fingerprint) must not delete \
4981 the entry the caller meant to retain, found {:?}",
4982 certificates_for_address.keys().collect::<Vec<_>>()
4983 );
4984 assert_eq!(
4985 certificates_for_address.len(),
4986 1,
4987 "an idempotent replace must not change the number of stored certificates"
4988 );
4989 }
4990
4991 #[test]
4996 fn replace_certificate_with_empty_names_round_trips_through_saved_state() {
4997 let certificate = CertificateAndKey {
4998 certificate: include_str!("../assets/certificate.pem").to_owned(),
4999 key: include_str!("../assets/key.pem").to_owned(),
5000 certificate_chain: Vec::new(),
5001 versions: Vec::new(),
5002 names: Vec::new(),
5003 };
5004 let fingerprint = certificate
5005 .fingerprint()
5006 .expect("test certificate fingerprint must be computable");
5007 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5008 let mut state = ConfigState::default();
5009 state
5010 .add_certificate(&AddCertificate {
5011 address,
5012 certificate: certificate.to_owned(),
5013 expired_at: None,
5014 })
5015 .expect("initial certificate insertion must succeed");
5016 assert!(
5017 !state.certificates[&SocketAddr::from(address)][&fingerprint]
5018 .names
5019 .is_empty(),
5020 "the fixture must expose at least one SAN or CN"
5021 );
5022
5023 state
5024 .replace_certificate(&ReplaceCertificate {
5025 address,
5026 new_certificate: certificate,
5027 old_fingerprint: fingerprint.to_string(),
5028 new_expired_at: None,
5029 })
5030 .expect("same-fingerprint replacement must succeed");
5031
5032 let requests = state.generate_requests();
5033 let mut replayed = ConfigState::default();
5034 for request in &requests {
5035 replayed
5036 .dispatch(request)
5037 .unwrap_or_else(|error| panic!("could not replay {request:?}: {error}"));
5038 }
5039 assert_eq!(
5040 replayed.certificates, state.certificates,
5041 "saved-state replay must preserve canonical certificate names"
5042 );
5043 }
5044
5045 fn ordered_saved_certificates(state: &ConfigState) -> Vec<(Fingerprint, Option<i64>)> {
5046 state
5047 .generate_requests()
5048 .into_iter()
5049 .filter_map(|request| match request.request_type {
5050 Some(RequestType::AddCertificate(add)) => Some((
5051 add.certificate
5052 .fingerprint()
5053 .expect("saved certificate fingerprint"),
5054 add.expired_at,
5055 )),
5056 _ => None,
5057 })
5058 .collect()
5059 }
5060
5061 fn distinct_test_certificates() -> (CertificateAndKey, CertificateAndKey) {
5062 (
5063 CertificateAndKey {
5064 certificate: include_str!("../../lib/assets/tests/certificate-1y.pem").to_owned(),
5065 key: include_str!("../../lib/assets/tests/key-1y.pem").to_owned(),
5066 names: vec!["shared.example.test".to_owned()],
5067 ..Default::default()
5068 },
5069 CertificateAndKey {
5070 certificate: include_str!("../../lib/assets/tests/certificate-2y.pem").to_owned(),
5071 key: include_str!("../../lib/assets/tests/key-2y.pem").to_owned(),
5072 names: vec!["shared.example.test".to_owned()],
5073 ..Default::default()
5074 },
5075 )
5076 }
5077
5078 fn third_test_certificate() -> CertificateAndKey {
5079 CertificateAndKey {
5080 certificate: include_str!("../../lib/assets/tests/certificate-3.pem").to_owned(),
5081 key: include_str!("../../lib/assets/tests/key.pem").to_owned(),
5082 names: vec!["shared.example.test".to_owned()],
5083 ..Default::default()
5084 }
5085 }
5086
5087 fn ordered_fingerprints(state: &ConfigState, address: SocketAddress) -> Vec<Fingerprint> {
5088 state
5089 .ordered_certificate_entries(address.into())
5090 .into_iter()
5091 .map(|(fingerprint, _, _)| fingerprint)
5092 .collect()
5093 }
5094
5095 #[test]
5100 fn saved_certificate_requests_preserve_expiration_and_insertion_order() {
5101 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5102 let (certificate_a, certificate_b) = distinct_test_certificates();
5103 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5104 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5105 let mut state = ConfigState::new();
5106 for (certificate, expired_at) in [
5107 (certificate_a, Some(2_100_000_000)),
5108 (certificate_b, Some(2_200_000_000)),
5109 ] {
5110 state
5111 .dispatch(
5112 &RequestType::AddCertificate(AddCertificate {
5113 address,
5114 certificate,
5115 expired_at,
5116 })
5117 .into(),
5118 )
5119 .expect("add certificate with expiration override");
5120 }
5121
5122 assert_eq!(
5123 ordered_saved_certificates(&state),
5124 vec![
5125 (fingerprint_a, Some(2_100_000_000)),
5126 (fingerprint_b, Some(2_200_000_000)),
5127 ],
5128 "saved-state requests must preserve certificate order and expiration overrides"
5129 );
5130
5131 use std::io::{Read, Seek, SeekFrom};
5132
5133 let mut file = tempfile::tempfile().expect("create SaveState file");
5134 state
5135 .write_requests_to_file(&mut file)
5136 .expect("serialize SaveState request stream");
5137 file.seek(SeekFrom::Start(0))
5138 .expect("rewind SaveState file");
5139 let mut bytes = Vec::new();
5140 file.read_to_end(&mut bytes).expect("read SaveState file");
5141 let (remaining, requests) = crate::parser::parse_several_requests::<WorkerRequest>(&bytes)
5142 .expect("parse LoadState request stream");
5143 assert!(remaining.is_empty(), "LoadState parser left trailing bytes");
5144 let mut replayed = ConfigState::new();
5145 for request in requests {
5146 replayed
5147 .dispatch(&request.content)
5148 .expect("LoadState request dispatch");
5149 }
5150 assert_eq!(
5151 replayed, state,
5152 "SaveState file parsing and LoadState dispatch must preserve all certificate metadata"
5153 );
5154 }
5155
5156 #[test]
5161 fn certificate_diff_replays_same_fingerprint_payload_and_expiration_changes() {
5162 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5163 let (mut certificate, _) = distinct_test_certificates();
5164 certificate.names = vec!["before.example.test".to_owned()];
5165 let fingerprint = certificate.fingerprint().expect("fingerprint certificate");
5166 let mut before = ConfigState::new();
5167 before
5168 .dispatch(
5169 &RequestType::AddCertificate(AddCertificate {
5170 address,
5171 certificate: certificate.to_owned(),
5172 expired_at: Some(2_100_000_000),
5173 })
5174 .into(),
5175 )
5176 .expect("add original certificate");
5177 let mut after = before.to_owned();
5178 certificate.names = vec!["after.example.test".to_owned()];
5179 after
5180 .dispatch(
5181 &RequestType::ReplaceCertificate(ReplaceCertificate {
5182 address,
5183 new_certificate: certificate,
5184 old_fingerprint: fingerprint.to_string(),
5185 new_expired_at: Some(2_200_000_000),
5186 })
5187 .into(),
5188 )
5189 .expect("replace same-fingerprint certificate payload");
5190
5191 let diff = before.diff(&after);
5192 assert!(
5193 diff.iter().any(|request| matches!(
5194 request.request_type,
5195 Some(RequestType::ReplaceCertificate(_))
5196 )),
5197 "same-fingerprint payload/expiration changes must emit ReplaceCertificate: {diff:?}"
5198 );
5199 }
5200
5201 #[test]
5205 fn certificate_diff_replays_order_only_changes() {
5206 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5207 let (certificate_a, certificate_b) = distinct_test_certificates();
5208 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5209 let mut before = ConfigState::new();
5210 let mut after = ConfigState::new();
5211 for certificate in [certificate_a.to_owned(), certificate_b.to_owned()] {
5212 before
5213 .dispatch(
5214 &RequestType::AddCertificate(AddCertificate {
5215 address,
5216 certificate,
5217 expired_at: Some(2_100_000_000),
5218 })
5219 .into(),
5220 )
5221 .expect("add before certificate");
5222 }
5223 for certificate in [certificate_b, certificate_a] {
5224 after
5225 .dispatch(
5226 &RequestType::AddCertificate(AddCertificate {
5227 address,
5228 certificate,
5229 expired_at: Some(2_100_000_000),
5230 })
5231 .into(),
5232 )
5233 .expect("add after certificate");
5234 }
5235
5236 let diff = before.diff(&after);
5237 assert_eq!(
5238 diff.len(),
5239 2,
5240 "A,B -> B,A needs only Remove(A), Add(A), not churn for B: {diff:?}"
5241 );
5242 assert!(
5243 matches!(
5244 &diff[0].request_type,
5245 Some(RequestType::RemoveCertificate(remove))
5246 if remove.fingerprint == fingerprint_a.to_string()
5247 ) && matches!(
5248 &diff[1].request_type,
5249 Some(RequestType::AddCertificate(add))
5250 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5251 ),
5252 "the minimal order-only sequence must move only certificate A to the end: {diff:?}"
5253 );
5254 }
5255
5256 #[test]
5257 fn certificate_diff_rotates_three_entries_by_moving_only_the_suffix() {
5258 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5259 let (certificate_a, certificate_b) = distinct_test_certificates();
5260 let certificate_c = third_test_certificate();
5261 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5262 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5263 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5264 let build = |certificates: Vec<CertificateAndKey>| {
5265 let mut state = ConfigState::new();
5266 for certificate in certificates {
5267 state
5268 .dispatch(
5269 &RequestType::AddCertificate(AddCertificate {
5270 address,
5271 certificate,
5272 expired_at: Some(2_100_000_000),
5273 })
5274 .into(),
5275 )
5276 .expect("add certificate");
5277 }
5278 state
5279 };
5280 let before = build(vec![
5281 certificate_a.to_owned(),
5282 certificate_b.to_owned(),
5283 certificate_c.to_owned(),
5284 ]);
5285 let after = build(vec![certificate_b, certificate_c, certificate_a]);
5286
5287 let diff = before.diff(&after);
5288 assert_eq!(
5289 diff.len(),
5290 2,
5291 "A,B,C -> B,C,A needs only Remove(A), Add(A): {diff:?}"
5292 );
5293 assert!(
5294 matches!(
5295 &diff[0].request_type,
5296 Some(RequestType::RemoveCertificate(remove))
5297 if remove.fingerprint == fingerprint_a.to_string()
5298 ) && matches!(
5299 &diff[1].request_type,
5300 Some(RequestType::AddCertificate(add))
5301 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5302 ),
5303 "the rotation must leave B and C in place: {diff:?}"
5304 );
5305 assert!(
5306 diff.iter().all(|request| !matches!(
5307 &request.request_type,
5308 Some(RequestType::RemoveCertificate(remove))
5309 if remove.fingerprint == fingerprint_b.to_string()
5310 || remove.fingerprint == fingerprint_c.to_string()
5311 )),
5312 "B and C are the retained target prefix and must not be removed: {diff:?}"
5313 );
5314
5315 let mut interrupted = before.to_owned();
5316 interrupted
5317 .dispatch(&diff[0])
5318 .expect("apply the minimal sequence's remove boundary");
5319 assert_ne!(
5320 ordered_fingerprints(&interrupted, address),
5321 ordered_fingerprints(&after, address),
5322 "the interruption point must expose a real partial state"
5323 );
5324 for request in interrupted.diff(&after) {
5325 interrupted
5326 .dispatch(&request)
5327 .expect("fresh diff recovers the interrupted rotation");
5328 }
5329 assert_eq!(
5330 ordered_fingerprints(&interrupted, address),
5331 vec![fingerprint_b, fingerprint_c, fingerprint_a],
5332 "recovery must converge on the target order and its final winner"
5333 );
5334 }
5335
5336 #[test]
5337 fn certificate_diff_inserts_around_a_retained_entry_without_readding_new_suffix() {
5338 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5339 let (certificate_a, certificate_b) = distinct_test_certificates();
5340 let certificate_c = third_test_certificate();
5341 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5342 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5343 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5344 let build = |certificates: Vec<CertificateAndKey>| {
5345 let mut state = ConfigState::new();
5346 for certificate in certificates {
5347 state
5348 .dispatch(
5349 &RequestType::AddCertificate(AddCertificate {
5350 address,
5351 certificate,
5352 expired_at: Some(2_100_000_000),
5353 })
5354 .into(),
5355 )
5356 .expect("add certificate");
5357 }
5358 state
5359 };
5360 let before = build(vec![certificate_b.to_owned()]);
5361 let after = build(vec![certificate_a, certificate_b, certificate_c]);
5362
5363 let diff = before.diff(&after);
5364 assert_eq!(
5365 diff.len(),
5366 4,
5367 "B -> A,B,C needs Add(A), Remove(B), Add(B), Add(C): {diff:?}"
5368 );
5369 assert!(
5370 matches!(
5371 &diff[0].request_type,
5372 Some(RequestType::AddCertificate(add))
5373 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5374 ) && matches!(
5375 &diff[1].request_type,
5376 Some(RequestType::RemoveCertificate(remove))
5377 if remove.fingerprint == fingerprint_b.to_string()
5378 ) && matches!(
5379 &diff[2].request_type,
5380 Some(RequestType::AddCertificate(add))
5381 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_b)
5382 ) && matches!(
5383 &diff[3].request_type,
5384 Some(RequestType::AddCertificate(add))
5385 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_c)
5386 ),
5387 "new certificate C must never be removed after insertion: {diff:?}"
5388 );
5389 assert!(
5390 diff.iter().all(|request| !matches!(
5391 &request.request_type,
5392 Some(RequestType::RemoveCertificate(remove))
5393 if remove.fingerprint == fingerprint_c.to_string()
5394 )),
5395 "the missing target suffix C is added once and never churned: {diff:?}"
5396 );
5397
5398 let mut interrupted = before.to_owned();
5399 interrupted
5400 .dispatch(&diff[0])
5401 .expect("apply the inserted target prefix before interruption");
5402 assert_ne!(
5403 ordered_fingerprints(&interrupted, address),
5404 ordered_fingerprints(&after, address),
5405 "the prefix interruption must expose a real partial state"
5406 );
5407 for request in interrupted.diff(&after) {
5408 interrupted
5409 .dispatch(&request)
5410 .expect("fresh diff recovers the interrupted insertion sequence");
5411 }
5412 assert_eq!(
5413 ordered_fingerprints(&interrupted, address),
5414 vec![fingerprint_a, fingerprint_b, fingerprint_c],
5415 "recovery must converge on the target order and final winner"
5416 );
5417 }
5418
5419 #[test]
5420 fn duplicate_certificate_add_keeps_payload_expiration_and_order() {
5421 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5422 let (certificate, _) = distinct_test_certificates();
5423 let mut state = ConfigState::new();
5424 state
5425 .add_certificate(&AddCertificate {
5426 address,
5427 certificate: certificate.to_owned(),
5428 expired_at: Some(2_100_000_000),
5429 })
5430 .expect("initial certificate");
5431 let before = state.to_owned();
5432 let mut differing_retry = certificate;
5433 differing_retry.names = vec!["different.example.test".to_owned()];
5434
5435 state
5436 .add_certificate(&AddCertificate {
5437 address,
5438 certificate: differing_retry,
5439 expired_at: Some(2_200_000_000),
5440 })
5441 .expect("duplicate fingerprint stays an accepted no-op");
5442
5443 assert_eq!(
5444 state, before,
5445 "duplicate AddCertificate must be a total no-op"
5446 );
5447 }
5448
5449 #[test]
5450 fn replacing_with_an_existing_identical_target_preserves_its_ordinal() {
5451 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5452 let (certificate_a, certificate_b) = distinct_test_certificates();
5453 let certificate_c = third_test_certificate();
5454 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5455 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5456 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5457 let mut state = ConfigState::new();
5458 for certificate in [certificate_a, certificate_b.to_owned(), certificate_c] {
5459 state
5460 .add_certificate(&AddCertificate {
5461 address,
5462 certificate,
5463 expired_at: Some(2_100_000_000),
5464 })
5465 .expect("initial certificate");
5466 }
5467
5468 state
5469 .replace_certificate(&ReplaceCertificate {
5470 address,
5471 new_certificate: certificate_b,
5472 old_fingerprint: fingerprint_a.to_string(),
5473 new_expired_at: Some(2_100_000_000),
5474 })
5475 .expect("replace A with already-stored B");
5476
5477 assert_eq!(
5478 ordered_fingerprints(&state, address),
5479 vec![fingerprint_b, fingerprint_c],
5480 "an already-stored replacement keeps B in place while removing A"
5481 );
5482 }
5483
5484 #[test]
5485 fn legacy_metadata_normalizes_once_and_the_second_replay_is_stable() {
5486 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5487 let (certificate_a, certificate_b) = distinct_test_certificates();
5488 let mut current = ConfigState::new();
5489 for (certificate, expired_at) in [
5490 (certificate_a, Some(2_100_000_000)),
5491 (certificate_b, Some(2_200_000_000)),
5492 ] {
5493 current
5494 .add_certificate(&AddCertificate {
5495 address,
5496 certificate,
5497 expired_at,
5498 })
5499 .expect("current certificate");
5500 }
5501 let mut legacy_json = serde_json::to_value(¤t).expect("serialize current state");
5502 legacy_json
5503 .as_object_mut()
5504 .expect("ConfigState JSON object")
5505 .remove("certificate_metadata")
5506 .expect("current state writes certificate metadata");
5507 let legacy: ConfigState =
5508 serde_json::from_value(legacy_json).expect("legacy state without sidecar");
5509 assert!(legacy.certificate_metadata.is_empty());
5510
5511 let mut first_replay = ConfigState::new();
5512 for request in legacy.generate_requests() {
5513 first_replay
5514 .dispatch(&request)
5515 .expect("legacy request stream replays");
5516 }
5517 assert_eq!(
5518 first_replay.certificate_metadata,
5519 legacy.normalized_certificate_metadata(),
5520 "the first replay records deterministic fallback ordinals"
5521 );
5522 assert_ne!(
5523 first_replay, legacy,
5524 "normalization is semantic state and must not be hidden from PartialEq"
5525 );
5526
5527 let mut second_replay = ConfigState::new();
5528 for request in first_replay.generate_requests() {
5529 second_replay
5530 .dispatch(&request)
5531 .expect("normalized request stream replays");
5532 }
5533 assert_eq!(
5534 second_replay, first_replay,
5535 "a second replay of normalized metadata must be byte/PartialEq stable"
5536 );
5537 assert_eq!(
5538 serde_json::to_value(&second_replay).expect("serialize second replay"),
5539 serde_json::to_value(&first_replay).expect("serialize first replay"),
5540 "the derived next-order cache must not affect serialized state"
5541 );
5542 }
5543
5544 fn legacy_state_with_certificates() -> (ConfigState, SocketAddress) {
5548 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5549 let (certificate_a, certificate_b) = distinct_test_certificates();
5550 let mut current = ConfigState::new();
5551 current
5552 .dispatch(&RequestType::AddHttpsListener(make_https_listener(address)).into())
5553 .expect("https listener");
5554 for cluster_id in ["cluster_1", "cluster_2"] {
5555 current
5556 .dispatch(
5557 &RequestType::AddCluster(Cluster {
5558 cluster_id: cluster_id.to_owned(),
5559 ..Default::default()
5560 })
5561 .into(),
5562 )
5563 .expect("cluster");
5564 current
5565 .dispatch(
5566 &RequestType::AddBackend(AddBackend {
5567 cluster_id: cluster_id.to_owned(),
5568 backend_id: format!("{cluster_id}-0"),
5569 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5570 ..Default::default()
5571 })
5572 .into(),
5573 )
5574 .expect("backend");
5575 current
5576 .dispatch(
5577 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
5578 cluster_id: Some(cluster_id.to_owned()),
5579 hostname: format!("{cluster_id}.example.test"),
5580 path: PathRule::prefix(String::from("/")),
5581 address,
5582 position: RulePosition::Tree.into(),
5583 ..Default::default()
5584 })
5585 .into(),
5586 )
5587 .expect("frontend");
5588 }
5589 for (certificate, expired_at) in [
5590 (certificate_a, Some(2_100_000_000)),
5591 (certificate_b, Some(2_200_000_000)),
5592 ] {
5593 current
5594 .add_certificate(&AddCertificate {
5595 address,
5596 certificate,
5597 expired_at,
5598 })
5599 .expect("certificate");
5600 }
5601 let mut json = serde_json::to_value(¤t).expect("serialize current state");
5602 json.as_object_mut()
5603 .expect("ConfigState JSON object")
5604 .remove("certificate_metadata")
5605 .expect("current state writes certificate metadata");
5606 let legacy: ConfigState = serde_json::from_value(json).expect("legacy state");
5607 assert!(legacy.certificate_metadata.is_empty());
5608 (legacy, address)
5609 }
5610
5611 fn replay_diff(from: &ConfigState, requests: &[Request]) -> ConfigState {
5612 let mut replayed = from.clone();
5613 for request in requests {
5614 replayed.dispatch(request).expect("diff request replays");
5615 }
5616 replayed
5617 }
5618
5619 #[test]
5620 fn diff_of_legacy_state_without_metadata_tolerates_a_listener_change() {
5621 let (legacy, address) = legacy_state_with_certificates();
5622 let mut desired = legacy.clone();
5623 desired
5624 .https_listeners
5625 .get_mut(&ListenerKey::from(address))
5626 .expect("listener")
5627 .front_timeout = 61;
5628
5629 let requests = legacy.diff(&desired);
5630
5631 assert!(!requests.is_empty(), "a listener change emits requests");
5632 assert!(
5633 requests.iter().all(|request| !matches!(
5634 request.request_type,
5635 Some(
5636 RequestType::AddCertificate(_)
5637 | RequestType::RemoveCertificate(_)
5638 | RequestType::ReplaceCertificate(_)
5639 )
5640 )),
5641 "retained certificates are not re-sent"
5642 );
5643 let replayed = replay_diff(&legacy, &requests);
5644 assert_eq!(replayed.https_listeners, desired.https_listeners);
5645 assert_eq!(replayed.certificates, desired.certificates);
5646 assert_eq!(replayed.https_fronts, legacy.https_fronts);
5647 assert_eq!(replayed.backends, legacy.backends);
5648 assert_eq!(
5649 replayed.normalized_certificate_metadata(),
5650 desired.normalized_certificate_metadata()
5651 );
5652 }
5653
5654 #[test]
5655 fn diff_of_legacy_state_without_metadata_tolerates_unrelated_removals() {
5656 let (legacy, address) = legacy_state_with_certificates();
5657 let mut desired = legacy.clone();
5658 desired
5659 .dispatch(
5660 &RequestType::RemoveBackend(RemoveBackend {
5661 cluster_id: "cluster_2".to_owned(),
5662 backend_id: "cluster_2-0".to_owned(),
5663 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5664 })
5665 .into(),
5666 )
5667 .expect("remove backend");
5668 desired
5669 .dispatch(
5670 &RequestType::RemoveHttpsFrontend(RequestHttpFrontend {
5671 cluster_id: Some("cluster_2".to_owned()),
5672 hostname: "cluster_2.example.test".to_owned(),
5673 path: PathRule::prefix(String::from("/")),
5674 address,
5675 position: RulePosition::Tree.into(),
5676 ..Default::default()
5677 })
5678 .into(),
5679 )
5680 .expect("remove frontend");
5681
5682 let requests = legacy.diff(&desired);
5683
5684 assert_eq!(requests.len(), 2, "exactly the two removals: {requests:?}");
5685 let replayed = replay_diff(&legacy, &requests);
5686 assert_eq!(replayed.certificates, legacy.certificates);
5687 assert_eq!(replayed.https_fronts, desired.https_fronts);
5688 assert_eq!(
5689 replayed.backends.get("cluster_1"),
5690 legacy.backends.get("cluster_1"),
5691 "the unaffected cluster keeps its backend"
5692 );
5693 assert!(
5694 replayed
5695 .backends
5696 .get("cluster_2")
5697 .is_none_or(|backends| backends.is_empty())
5698 );
5699 assert_eq!(
5700 replayed.normalized_certificate_metadata(),
5701 desired.normalized_certificate_metadata()
5702 );
5703 }
5704
5705 #[test]
5709 fn diff_still_reports_explicit_metadata_differences() {
5710 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5711 let (certificate_a, certificate_b) = distinct_test_certificates();
5712 let build = |first: (&CertificateAndKey, u64), second: (&CertificateAndKey, u64)| {
5713 let mut state = ConfigState::new();
5714 for (certificate, expired_at) in [first, second] {
5715 state
5716 .add_certificate(&AddCertificate {
5717 address,
5718 certificate: certificate.clone(),
5719 expired_at: Some(expired_at as i64),
5720 })
5721 .expect("certificate");
5722 }
5723 state
5724 };
5725 let base = build(
5726 (&certificate_a, 2_100_000_000),
5727 (&certificate_b, 2_200_000_000),
5728 );
5729 let other_expiry = build(
5730 (&certificate_a, 2_100_000_000),
5731 (&certificate_b, 2_300_000_000),
5732 );
5733 let other_order = build(
5734 (&certificate_b, 2_200_000_000),
5735 (&certificate_a, 2_100_000_000),
5736 );
5737
5738 assert_ne!(
5739 base.normalized_certificate_metadata(),
5740 other_expiry.normalized_certificate_metadata(),
5741 "expired_at survives normalization"
5742 );
5743 assert_ne!(
5744 base.normalized_certificate_metadata(),
5745 other_order.normalized_certificate_metadata(),
5746 "insertion order survives normalization"
5747 );
5748 for other in [&other_expiry, &other_order] {
5749 let requests = base.diff(other);
5750 assert!(
5751 requests.iter().any(|request| matches!(
5752 request.request_type,
5753 Some(
5754 RequestType::AddCertificate(_)
5755 | RequestType::RemoveCertificate(_)
5756 | RequestType::ReplaceCertificate(_)
5757 )
5758 )),
5759 "an explicit metadata difference yields certificate operations"
5760 );
5761 let replayed = replay_diff(&base, &requests);
5762 assert_eq!(
5763 replayed.normalized_certificate_metadata(),
5764 other.normalized_certificate_metadata()
5765 );
5766 }
5767 }
5768
5769 #[test]
5770 fn legacy_reader_ignores_current_certificate_metadata_but_keeps_material() {
5771 #[derive(serde::Deserialize)]
5772 struct LegacyConfigStateMirror {
5773 certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
5774 }
5775
5776 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5777 let socket_address: SocketAddr = address.into();
5778 let (certificate, _) = distinct_test_certificates();
5779 let fingerprint = certificate.fingerprint().expect("fingerprint");
5780 let mut current = ConfigState::new();
5781 current
5782 .add_certificate(&AddCertificate {
5783 address,
5784 certificate: certificate.to_owned(),
5785 expired_at: Some(2_100_000_000),
5786 })
5787 .expect("current certificate");
5788
5789 let legacy: LegacyConfigStateMirror = serde_json::from_value(
5790 serde_json::to_value(¤t).expect("serialize current ConfigState"),
5791 )
5792 .expect("an old serde reader must ignore the unknown metadata field");
5793
5794 assert_eq!(
5795 legacy.certificates[&socket_address][&fingerprint], certificate,
5796 "new-to-old JSON remains syntactically readable, with metadata intentionally lost"
5797 );
5798 }
5799
5800 #[test]
5801 fn malformed_legacy_metadata_is_unchanged_on_refusal_then_normalized_on_success() {
5802 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5803 let socket_address: SocketAddr = address.into();
5804 let (certificate_a, certificate_b) = distinct_test_certificates();
5805 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5806 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5807 let mut state = ConfigState::new();
5808 state.certificates.insert(
5809 socket_address,
5810 HashMap::from([(fingerprint_a.to_owned(), certificate_a)]),
5811 );
5812 state.certificate_metadata.insert(
5813 socket_address,
5814 CertificateMetadataIndex {
5815 entries: HashMap::from([
5816 (
5817 fingerprint_a,
5818 CertificateMetadata {
5819 expired_at: Some(2_100_000_000),
5820 insertion_order: 7,
5821 },
5822 ),
5823 (
5824 Fingerprint(vec![0xff]),
5825 CertificateMetadata {
5826 expired_at: Some(9),
5827 insertion_order: 7,
5828 },
5829 ),
5830 ]),
5831 next_order: None,
5832 },
5833 );
5834 let before = state.to_owned();
5835
5836 let error = state.add_certificate(&AddCertificate {
5837 address,
5838 certificate: CertificateAndKey {
5839 certificate: "not PEM".to_owned(),
5840 ..Default::default()
5841 },
5842 expired_at: None,
5843 });
5844 assert!(error.is_err(), "malformed certificate must be refused");
5845 assert_eq!(
5846 state, before,
5847 "a refusal must not normalize or mutate legacy state"
5848 );
5849
5850 state
5851 .add_certificate(&AddCertificate {
5852 address,
5853 certificate: certificate_b,
5854 expired_at: Some(2_200_000_000),
5855 })
5856 .expect("valid mutation normalizes legacy metadata");
5857 let metadata = &state.certificate_metadata[&socket_address].entries;
5858 assert_eq!(metadata.len(), 2, "orphan metadata must be pruned");
5859 assert_eq!(metadata[&fingerprint_b].insertion_order, 1);
5860 assert_eq!(
5861 metadata
5862 .values()
5863 .map(|entry| entry.insertion_order)
5864 .collect::<BTreeSet<_>>(),
5865 BTreeSet::from([0, 1]),
5866 "normalized ordinals must be unique and contiguous"
5867 );
5868 }
5869
5870 #[test]
5885 fn remove_certificate_evicts_the_address_when_its_last_certificate_is_removed() {
5886 let certificate = CertificateAndKey {
5887 certificate: include_str!("../assets/certificate.pem").to_owned(),
5888 key: include_str!("../assets/key.pem").to_owned(),
5889 certificate_chain: Vec::new(),
5890 versions: Vec::new(),
5891 names: Vec::new(),
5892 };
5893 let fingerprint = certificate
5894 .fingerprint()
5895 .expect("test certificate fingerprint must be computable");
5896 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5897
5898 let mut state = ConfigState::default();
5899 state
5900 .add_certificate(&AddCertificate {
5901 address,
5902 certificate,
5903 expired_at: None,
5904 })
5905 .expect("initial certificate insertion must succeed");
5906
5907 state
5908 .remove_certificate(&RemoveCertificate {
5909 address,
5910 fingerprint: fingerprint.to_string(),
5911 })
5912 .expect("removing the only certificate must succeed");
5913
5914 assert!(
5915 !state.certificates.contains_key(&SocketAddr::from(address)),
5916 "removing an address's last certificate must evict the address key too, \
5917 found {:?}",
5918 state.certificates.get(&SocketAddr::from(address))
5919 );
5920
5921 let _ = state.produce_initial_state();
5925 }
5926
5927 #[test]
5939 fn add_certificate_with_valid_pem_armor_but_invalid_x509_leaves_no_bucket() {
5940 let junk_pem = "-----BEGIN CERTIFICATE-----\n\
5943 dGhpcyBpcyBub3QgYSB2YWxpZCB4NTA5IGRlciBzdHJ1Y3R1cmUgYXQgYWxsLCBq\n\
5944 dXN0IGZpbGxlciBieXRlcyBwYWRkZWQgb3V0\n\
5945 -----END CERTIFICATE-----\n";
5946 let certificate = CertificateAndKey {
5947 certificate: junk_pem.to_owned(),
5948 key: include_str!("../assets/key.pem").to_owned(),
5949 certificate_chain: Vec::new(),
5950 versions: Vec::new(),
5951 names: Vec::new(),
5952 };
5953 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5954
5955 let mut state = ConfigState::default();
5956 assert!(
5957 !state.certificates.contains_key(&SocketAddr::from(address)),
5958 "the address must be absent before the add attempt"
5959 );
5960
5961 let error = state
5962 .add_certificate(&AddCertificate {
5963 address,
5964 certificate,
5965 expired_at: None,
5966 })
5967 .expect_err("valid PEM armor around invalid X.509 content must be rejected");
5968 assert!(
5969 matches!(error, StateError::AddCertificate(_)),
5970 "expected StateError::AddCertificate, got {error:?}"
5971 );
5972
5973 assert!(
5974 !state.certificates.contains_key(&SocketAddr::from(address)),
5975 "a rejected add_certificate must leave no bucket for a previously-absent address, \
5976 found {:?}",
5977 state.certificates.get(&SocketAddr::from(address))
5978 );
5979 }
5980
5981 #[test]
5982 fn frontend_state_error_redacts_display_key() {
5983 const HOSTNAME_SECRET: &str = "STATE_ERROR_HOSTNAME_SECRET_SENTINEL";
5984 const PATH_SECRET: &str = "STATE_ERROR_PATH_SECRET_SENTINEL";
5985 const METHOD_SECRET: &str = "STATE_ERROR_METHOD_SECRET_SENTINEL";
5986
5987 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
5988 let front = RequestHttpFrontend {
5989 cluster_id: Some("cluster".to_owned()),
5990 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
5991 hostname: long_value(HOSTNAME_SECRET),
5992 path: PathRule {
5993 kind: PathRuleKind::Prefix as i32,
5994 value: long_value(PATH_SECRET),
5995 },
5996 method: Some(long_value(METHOD_SECRET)),
5997 position: RulePosition::Tree as i32,
5998 tags: BTreeMap::new(),
5999 redirect: None,
6000 redirect_scheme: None,
6001 redirect_template: None,
6002 rewrite_host: None,
6003 rewrite_path: None,
6004 rewrite_port: None,
6005 required_auth: None,
6006 headers: Vec::new(),
6007 hsts: None,
6008 header_predicates: Vec::new(),
6009 };
6010 let mut state = ConfigState::default();
6011 state
6012 .add_http_frontend(&front)
6013 .expect("first frontend insertion must succeed");
6014 assert!(
6015 state
6016 .http_fronts
6017 .keys()
6018 .next()
6019 .expect("raw state key must be retained")
6020 .contains(HOSTNAME_SECRET),
6021 "state storage must retain the raw operator-facing frontend key"
6022 );
6023
6024 let error = state
6025 .add_http_frontend(&front)
6026 .expect_err("duplicate frontend insertion must return StateError::Exists");
6027 let raw_frontend_key = front.to_string();
6028 match &error {
6029 StateError::Exists { id, .. } => assert_eq!(
6030 id, &raw_frontend_key,
6031 "StateError must preserve the public raw frontend identifier"
6032 ),
6033 other => panic!("expected StateError::Exists, got {other:?}"),
6034 }
6035 for (label, output) in [
6036 ("Display", error.to_string()),
6037 ("Debug", format!("{error:?}")),
6038 ] {
6039 for secret in [HOSTNAME_SECRET, PATH_SECRET, METHOD_SECRET] {
6040 assert!(
6041 !output.contains(secret),
6042 "StateError {label} leaked frontend marker {secret}"
6043 );
6044 }
6045 let metadata = format!("id_bytes={}", raw_frontend_key.len());
6046 assert!(
6047 output.contains(&metadata),
6048 "StateError {label} omitted bounded metadata {metadata}: {output}"
6049 );
6050 assert!(
6051 output.len() <= 512,
6052 "StateError {label} output is not bounded: {} bytes",
6053 output.len()
6054 );
6055 }
6056 }
6057
6058 #[test]
6059 fn state_error_formatting_bounds_every_string_bearing_variant() {
6060 const SECRET: &str = "STATE_ERROR_VARIANT_SECRET_SENTINEL";
6061
6062 let long_value = || format!("{SECRET}{}", "x".repeat(4096));
6063 let errors = [
6064 StateError::NotFound {
6065 kind: ObjectKind::Backend,
6066 id: long_value(),
6067 },
6068 StateError::Exists {
6069 kind: ObjectKind::Cluster,
6070 id: long_value(),
6071 },
6072 StateError::ClusterNotFound {
6073 kind: ObjectKind::Backend,
6074 cluster_id: long_value(),
6075 },
6076 StateError::AddCertificate(CertificateError::ParsePEMCertificate(long_value())),
6077 StateError::RemoveCertificate(long_value()),
6078 StateError::ReplaceCertificate(long_value()),
6079 StateError::FrontendConversion {
6080 frontend: long_value(),
6081 error: long_value(),
6082 },
6083 StateError::FileError(std::io::Error::other(long_value())),
6084 StateError::InvalidTcpFrontend {
6085 address: "127.0.0.1:443"
6086 .parse()
6087 .expect("test TCP frontend address must parse"),
6088 reason: long_value(),
6089 },
6090 StateError::UdpFrontendAddressTaken {
6091 address: "127.0.0.1:5353"
6092 .parse()
6093 .expect("test UDP frontend address must parse"),
6094 cluster_id: long_value().into(),
6095 current_cluster_id: long_value().into(),
6096 current_tags: BTreeMap::from([(long_value(), long_value())]),
6097 },
6098 ];
6099
6100 for error in errors {
6101 for (label, output) in [
6102 ("Display", error.to_string()),
6103 ("Debug", format!("{error:?}")),
6104 ] {
6105 assert!(
6106 !output.contains(SECRET),
6107 "StateError {label} leaked a string-bearing variant: {output}"
6108 );
6109 assert!(
6110 output.len() <= 512,
6111 "StateError {label} is not bounded: {} bytes",
6112 output.len()
6113 );
6114 }
6115 }
6116 }
6117
6118 #[test]
6119 fn tcp_frontend_not_found_error_retains_the_raw_identity() {
6120 const TCP_SECRET: &str = "STATE_ERROR_TCP_IDENTITY_SECRET_SENTINEL";
6121
6122 let long_value = |suffix: &str| format!("{TCP_SECRET}_{suffix}{}", "x".repeat(1024));
6123 let frontend = RequestTcpFrontend {
6124 cluster_id: long_value("cluster"),
6125 address: SocketAddress::new_v4(127, 0, 0, 1, 443),
6126 tags: BTreeMap::from([(long_value("tag-key"), long_value("tag-value"))]),
6127 sni: Some(long_value("sni")),
6128 alpn: vec![long_value("alpn")],
6129 };
6130 let error = ConfigState::default()
6131 .remove_tcp_frontend(&frontend)
6132 .expect_err("removing from a missing cluster must return StateError::NotFound");
6133
6134 match &error {
6135 StateError::NotFound { id, .. } => assert!(
6136 id.contains(TCP_SECRET),
6137 "StateError must retain the raw TCP frontend identity: {id}"
6138 ),
6139 other => panic!("expected StateError::NotFound, got {other:?}"),
6140 }
6141 for output in [error.to_string(), format!("{error:?}")] {
6142 assert!(
6143 !output.contains(TCP_SECRET),
6144 "StateError formatting leaked the retained TCP identity: {output}"
6145 );
6146 assert!(
6147 output.len() <= 512,
6148 "StateError formatting is not bounded: {} bytes",
6149 output.len()
6150 );
6151 }
6152 }
6153
6154 #[test]
6155 fn retained_state_debug_redacts_http_frontend_and_collection_data() {
6156 const CLUSTER_SECRET: &str = "RETAINED_CLUSTER_SECRET_SENTINEL";
6157 const HOSTNAME_SECRET: &str = "RETAINED_HOSTNAME_SECRET_SENTINEL";
6158 const PATH_SECRET: &str = "RETAINED_PATH_SECRET_SENTINEL";
6159 const METHOD_SECRET: &str = "RETAINED_METHOD_SECRET_SENTINEL";
6160 const TAG_KEY_SECRET: &str = "RETAINED_TAG_KEY_SECRET_SENTINEL";
6161 const TAG_VALUE_SECRET: &str = "RETAINED_TAG_VALUE_SECRET_SENTINEL";
6162 const REDIRECT_TEMPLATE_SECRET: &str = "RETAINED_REDIRECT_TEMPLATE_SECRET_SENTINEL";
6163 const REWRITE_HOST_SECRET: &str = "RETAINED_REWRITE_HOST_SECRET_SENTINEL";
6164 const REWRITE_PATH_SECRET: &str = "RETAINED_REWRITE_PATH_SECRET_SENTINEL";
6165 const HEADER_KEY_SECRET: &str = "RETAINED_HEADER_KEY_SECRET_SENTINEL";
6166 const HEADER_VALUE_SECRET: &str = "RETAINED_HEADER_VALUE_SECRET_SENTINEL";
6167 const REQUEST_COUNT_SECRET: &str = "RETAINED_REQUEST_COUNT_SECRET_SENTINEL";
6168
6169 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
6170 let request = RequestHttpFrontend {
6171 cluster_id: Some(long_value(CLUSTER_SECRET)),
6172 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
6173 hostname: long_value(HOSTNAME_SECRET),
6174 path: PathRule {
6175 kind: PathRuleKind::Regex as i32,
6176 value: long_value(PATH_SECRET),
6177 },
6178 method: Some(long_value(METHOD_SECRET)),
6179 position: RulePosition::Tree as i32,
6180 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
6181 redirect: Some(RedirectPolicy::PermanentRedirect as i32),
6182 redirect_scheme: Some(RedirectScheme::UseHttps as i32),
6183 redirect_template: Some(long_value(REDIRECT_TEMPLATE_SECRET)),
6184 rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
6185 rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
6186 rewrite_port: Some(9443),
6187 required_auth: Some(true),
6188 headers: vec![Header {
6189 position: HeaderPosition::Both as i32,
6190 key: long_value(HEADER_KEY_SECRET),
6191 val: long_value(HEADER_VALUE_SECRET),
6192 }],
6193 hsts: Some(HstsConfig {
6194 enabled: Some(true),
6195 max_age: Some(31_536_000),
6196 include_subdomains: Some(true),
6197 preload: Some(false),
6198 force_replace_backend: Some(true),
6199 }),
6200 header_predicates: Vec::new(),
6201 };
6202 let retained = request
6203 .clone()
6204 .to_frontend()
6205 .expect("adversarial frontend must convert");
6206 let retained_debug = format!("{retained:?}");
6207
6208 let mut state = ConfigState::default();
6209 state
6210 .dispatch(&RequestType::AddHttpFrontend(request).into())
6211 .expect("adversarial frontend must be retained");
6212 state
6213 .request_counts
6214 .insert(long_value(REQUEST_COUNT_SECRET), 7);
6215 let retained_key = state
6216 .http_fronts
6217 .keys()
6218 .next()
6219 .expect("dispatch must retain the frontend's Display key");
6220 assert!(retained_key.contains(HOSTNAME_SECRET));
6221 assert!(retained_key.contains(PATH_SECRET));
6222 assert!(retained_key.contains(METHOD_SECRET));
6223 let state_debug = format!("{state:?}");
6224
6225 let secrets = [
6226 CLUSTER_SECRET,
6227 HOSTNAME_SECRET,
6228 PATH_SECRET,
6229 METHOD_SECRET,
6230 TAG_KEY_SECRET,
6231 TAG_VALUE_SECRET,
6232 REDIRECT_TEMPLATE_SECRET,
6233 REWRITE_HOST_SECRET,
6234 REWRITE_PATH_SECRET,
6235 HEADER_KEY_SECRET,
6236 HEADER_VALUE_SECRET,
6237 REQUEST_COUNT_SECRET,
6238 ];
6239 for (label, output) in [
6240 ("HttpFrontend", &retained_debug),
6241 ("ConfigState", &state_debug),
6242 ] {
6243 for secret in secrets {
6244 assert!(
6245 !output.contains(secret),
6246 "{label} Debug leaked retained marker {secret}"
6247 );
6248 }
6249 assert!(
6250 output.len() <= 2048,
6251 "{label} Debug output is not bounded: {} bytes",
6252 output.len()
6253 );
6254 }
6255
6256 for metadata in [
6257 format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
6258 "address: 127.0.0.1:8443".to_owned(),
6259 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
6260 "path_kind: 1".to_owned(),
6261 format!("path_len: {}", long_value(PATH_SECRET).len()),
6262 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
6263 "position: Tree".to_owned(),
6264 "tags_count: Some(1)".to_owned(),
6265 format!(
6266 "tags_len: Some({})",
6267 long_value(TAG_KEY_SECRET).len() + long_value(TAG_VALUE_SECRET).len()
6268 ),
6269 "redirect: Some(4)".to_owned(),
6270 "redirect_scheme: Some(2)".to_owned(),
6271 format!(
6272 "redirect_template_len: Some({})",
6273 long_value(REDIRECT_TEMPLATE_SECRET).len()
6274 ),
6275 format!(
6276 "rewrite_host_len: Some({})",
6277 long_value(REWRITE_HOST_SECRET).len()
6278 ),
6279 format!(
6280 "rewrite_path_len: Some({})",
6281 long_value(REWRITE_PATH_SECRET).len()
6282 ),
6283 "rewrite_port: Some(9443)".to_owned(),
6284 "required_auth: Some(true)".to_owned(),
6285 "headers_count: 1".to_owned(),
6286 format!(
6287 "headers_len: {}",
6288 long_value(HEADER_KEY_SECRET).len() + long_value(HEADER_VALUE_SECRET).len()
6289 ),
6290 "hsts: Some(HstsConfig".to_owned(),
6291 ] {
6292 assert!(
6293 retained_debug.contains(&metadata),
6294 "HttpFrontend Debug omitted safe metadata {metadata}: {retained_debug}"
6295 );
6296 }
6297 for metadata in [
6298 "http_frontends_count: 1",
6299 "backends_count: 0",
6300 "tcp_frontends_count: 0",
6301 "udp_frontends_count: 0",
6302 "certificates_count: 0",
6303 "request_counts_count: 2",
6304 ] {
6305 assert!(
6306 state_debug.contains(metadata),
6307 "ConfigState Debug omitted count metadata {metadata}: {state_debug}"
6308 );
6309 }
6310 }
6311
6312 fn add_clusters(state: &mut ConfigState, cluster_ids: &[&str]) {
6315 for cluster_id in cluster_ids {
6316 state
6317 .dispatch(
6318 &RequestType::AddCluster(Cluster {
6319 cluster_id: cluster_id.to_string(),
6320 ..Default::default()
6321 })
6322 .into(),
6323 )
6324 .expect("Could not add the test cluster");
6325 }
6326 }
6327
6328 #[test]
6329 fn serialize() {
6330 let mut state: ConfigState = Default::default();
6331 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6332 state
6333 .dispatch(
6334 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6335 cluster_id: Some(String::from("cluster_1")),
6336 hostname: String::from("lolcatho.st:8080"),
6337 path: PathRule::prefix(String::from("/")),
6338 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6339 position: RulePosition::Tree.into(),
6340 ..Default::default()
6341 })
6342 .into(),
6343 )
6344 .expect("Could not execute request");
6345 state
6346 .dispatch(
6347 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6348 cluster_id: Some(String::from("cluster_2")),
6349 hostname: String::from("test.local"),
6350 path: PathRule::prefix(String::from("/abc")),
6351 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6352 position: RulePosition::Pre.into(),
6353 ..Default::default()
6354 })
6355 .into(),
6356 )
6357 .expect("Could not execute request");
6358 state
6359 .dispatch(
6360 &RequestType::AddBackend(AddBackend {
6361 cluster_id: String::from("cluster_1"),
6362 backend_id: String::from("cluster_1-0"),
6363 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6364 ..Default::default()
6365 })
6366 .into(),
6367 )
6368 .expect("Could not execute request");
6369 state
6370 .dispatch(
6371 &RequestType::AddBackend(AddBackend {
6372 cluster_id: String::from("cluster_1"),
6373 backend_id: String::from("cluster_1-1"),
6374 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
6375 ..Default::default()
6376 })
6377 .into(),
6378 )
6379 .expect("Could not execute request");
6380 state
6381 .dispatch(
6382 &RequestType::AddBackend(AddBackend {
6383 cluster_id: String::from("cluster_2"),
6384 backend_id: String::from("cluster_2-0"),
6385 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6386 ..Default::default()
6387 })
6388 .into(),
6389 )
6390 .expect("Could not execute request");
6391 state
6392 .dispatch(
6393 &RequestType::AddBackend(AddBackend {
6394 cluster_id: String::from("cluster_1"),
6395 backend_id: String::from("cluster_1-3"),
6396 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6397 ..Default::default()
6398 })
6399 .into(),
6400 )
6401 .expect("Could not execute request");
6402 state
6403 .dispatch(
6404 &RequestType::RemoveBackend(RemoveBackend {
6405 cluster_id: String::from("cluster_1"),
6406 backend_id: String::from("cluster_1-3"),
6407 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6408 })
6409 .into(),
6410 )
6411 .expect("Could not execute request");
6412
6413 }
6423
6424 #[test]
6425 fn diff() {
6426 let mut state: ConfigState = Default::default();
6427 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6428 state
6429 .dispatch(
6430 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6431 cluster_id: Some(String::from("cluster_1")),
6432 hostname: String::from("lolcatho.st:8080"),
6433 path: PathRule::prefix(String::from("/")),
6434 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6435 position: RulePosition::Post.into(),
6436 ..Default::default()
6437 })
6438 .into(),
6439 )
6440 .expect("Could not execute request");
6441 state
6442 .dispatch(
6443 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6444 cluster_id: Some(String::from("cluster_2")),
6445 hostname: String::from("test.local"),
6446 path: PathRule::prefix(String::from("/abc")),
6447 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6448 ..Default::default()
6449 })
6450 .into(),
6451 )
6452 .expect("Could not execute request");
6453 state
6454 .dispatch(
6455 &RequestType::AddBackend(AddBackend {
6456 cluster_id: String::from("cluster_1"),
6457 backend_id: String::from("cluster_1-0"),
6458 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6459 load_balancing_parameters: Some(LoadBalancingParams::default()),
6460 ..Default::default()
6461 })
6462 .into(),
6463 )
6464 .expect("Could not execute request");
6465 state
6466 .dispatch(
6467 &RequestType::AddBackend(AddBackend {
6468 cluster_id: String::from("cluster_1"),
6469 backend_id: String::from("cluster_1-1"),
6470 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6471 load_balancing_parameters: Some(LoadBalancingParams::default()),
6472 ..Default::default()
6473 })
6474 .into(),
6475 )
6476 .expect("Could not execute request");
6477 state
6478 .dispatch(
6479 &RequestType::AddBackend(AddBackend {
6480 cluster_id: String::from("cluster_2"),
6481 backend_id: String::from("cluster_2-0"),
6482 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6483 load_balancing_parameters: Some(LoadBalancingParams::default()),
6484 ..Default::default()
6485 })
6486 .into(),
6487 )
6488 .expect("Could not execute request");
6489 state
6490 .dispatch(
6491 &RequestType::AddCluster(Cluster {
6492 cluster_id: String::from("cluster_2"),
6493 sticky_session: true,
6494 https_redirect: true,
6495 ..Default::default()
6496 })
6497 .into(),
6498 )
6499 .expect("Could not execute request");
6500
6501 let mut state2: ConfigState = Default::default();
6502 add_clusters(&mut state2, &["cluster_1"]);
6503 state2
6504 .dispatch(
6505 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6506 cluster_id: Some(String::from("cluster_1")),
6507 hostname: String::from("lolcatho.st:8080"),
6508 path: PathRule::prefix(String::from("/")),
6509 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6510 position: RulePosition::Post.into(),
6511 ..Default::default()
6512 })
6513 .into(),
6514 )
6515 .expect("Could not execute request");
6516 state2
6517 .dispatch(
6518 &RequestType::AddBackend(AddBackend {
6519 cluster_id: String::from("cluster_1"),
6520 backend_id: String::from("cluster_1-0"),
6521 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6522 load_balancing_parameters: Some(LoadBalancingParams::default()),
6523 ..Default::default()
6524 })
6525 .into(),
6526 )
6527 .expect("Could not execute request");
6528 state2
6529 .dispatch(
6530 &RequestType::AddBackend(AddBackend {
6531 cluster_id: String::from("cluster_1"),
6532 backend_id: String::from("cluster_1-1"),
6533 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6534 load_balancing_parameters: Some(LoadBalancingParams::default()),
6535 ..Default::default()
6536 })
6537 .into(),
6538 )
6539 .expect("Could not execute request");
6540 state2
6541 .dispatch(
6542 &RequestType::AddBackend(AddBackend {
6543 cluster_id: String::from("cluster_1"),
6544 backend_id: String::from("cluster_1-2"),
6545 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6546 load_balancing_parameters: Some(LoadBalancingParams::default()),
6547 ..Default::default()
6548 })
6549 .into(),
6550 )
6551 .expect("Could not execute request");
6552 state2
6553 .dispatch(
6554 &RequestType::AddCluster(Cluster {
6555 cluster_id: String::from("cluster_3"),
6556 sticky_session: false,
6557 https_redirect: false,
6558 ..Default::default()
6559 })
6560 .into(),
6561 )
6562 .expect("Could not execute request");
6563
6564 let e: Vec<Request> = vec![
6568 RequestType::AddBackend(AddBackend {
6569 cluster_id: String::from("cluster_1"),
6570 backend_id: String::from("cluster_1-2"),
6571 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6572 load_balancing_parameters: Some(LoadBalancingParams::default()),
6573 ..Default::default()
6574 })
6575 .into(),
6576 RequestType::RemoveCluster(String::from("cluster_2")).into(),
6577 RequestType::AddCluster(Cluster {
6578 cluster_id: String::from("cluster_3"),
6579 sticky_session: false,
6580 https_redirect: false,
6581 ..Default::default()
6582 })
6583 .into(),
6584 ];
6585 let expected_diff: HashSet<&Request> = HashSet::from_iter(e.iter());
6586
6587 let d = state.diff(&state2);
6588 let diff = HashSet::from_iter(d.iter());
6589 println!("diff requests:\n{diff:#?}\n");
6590 println!("expected diff requests:\n{expected_diff:#?}\n");
6591
6592 let hash1 = state.hash_state();
6593 let hash2 = state2.hash_state();
6594 let mut state3 = state.clone();
6595 state3
6596 .dispatch(
6597 &RequestType::AddBackend(AddBackend {
6598 cluster_id: String::from("cluster_1"),
6599 backend_id: String::from("cluster_1-2"),
6600 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6601 load_balancing_parameters: Some(LoadBalancingParams::default()),
6602 ..Default::default()
6603 })
6604 .into(),
6605 )
6606 .expect("Could not execute request");
6607 let hash3 = state3.hash_state();
6608 println!("state 1 hashes: {hash1:#?}");
6609 println!("state 2 hashes: {hash2:#?}");
6610 println!("state 3 hashes: {hash3:#?}");
6611
6612 assert_eq!(diff, expected_diff);
6613 }
6614
6615 fn state_with_clusters_owning_every_object_kind(cluster_ids: &[&str]) -> ConfigState {
6621 let mut state = ConfigState::new();
6622 for cluster_id in cluster_ids {
6623 let port = match *cluster_id {
6624 "cascade_a" => 9200,
6625 "cascade_b" => 9300,
6626 other => panic!("no test ports reserved for {other}"),
6627 };
6628 let requests: Vec<Request> = vec![
6629 RequestType::AddCluster(Cluster {
6630 cluster_id: cluster_id.to_string(),
6631 ..Default::default()
6632 })
6633 .into(),
6634 RequestType::AddHttpFrontend(RequestHttpFrontend {
6635 cluster_id: Some(cluster_id.to_string()),
6636 hostname: format!("{cluster_id}.test"),
6637 path: PathRule::prefix(String::from("/")),
6638 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6639 ..Default::default()
6640 })
6641 .into(),
6642 RequestType::AddHttpsFrontend(RequestHttpFrontend {
6643 cluster_id: Some(cluster_id.to_string()),
6644 hostname: format!("{cluster_id}.test"),
6645 path: PathRule::prefix(String::from("/")),
6646 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
6647 ..Default::default()
6648 })
6649 .into(),
6650 RequestType::AddTcpFrontend(RequestTcpFrontend {
6651 cluster_id: cluster_id.to_string(),
6652 address: SocketAddress::new_v4(127, 0, 0, 1, port),
6653 ..Default::default()
6654 })
6655 .into(),
6656 RequestType::AddUdpFrontend(RequestUdpFrontend {
6657 cluster_id: cluster_id.to_string(),
6658 address: SocketAddress::new_v4(127, 0, 0, 1, port + 1),
6659 ..Default::default()
6660 })
6661 .into(),
6662 RequestType::AddBackend(AddBackend {
6663 cluster_id: cluster_id.to_string(),
6664 backend_id: format!("{cluster_id}-0"),
6665 address: SocketAddress::new_v4(127, 0, 0, 1, port + 2),
6666 load_balancing_parameters: Some(LoadBalancingParams::default()),
6667 ..Default::default()
6668 })
6669 .into(),
6670 ];
6671 for request in &requests {
6672 state
6673 .dispatch(request)
6674 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6675 }
6676 }
6677 state
6678 .dispatch(&RequestType::AddHttpFrontend(cascade_deny_route()).into())
6679 .expect("could not add the deny route");
6680 state
6681 }
6682
6683 fn cascade_deny_route() -> RequestHttpFrontend {
6685 RequestHttpFrontend {
6686 cluster_id: None,
6687 hostname: String::from("cascade_a.test"),
6688 path: PathRule::prefix(String::from("/deny")),
6689 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6690 ..Default::default()
6691 }
6692 }
6693
6694 fn cluster_named_by(request: &Request) -> Option<&str> {
6696 match request.request_type.as_ref()? {
6697 RequestType::AddCluster(cluster) => Some(&cluster.cluster_id),
6698 RequestType::RemoveCluster(cluster_id) => Some(cluster_id),
6699 RequestType::AddHttpFrontend(front)
6700 | RequestType::RemoveHttpFrontend(front)
6701 | RequestType::AddHttpsFrontend(front)
6702 | RequestType::RemoveHttpsFrontend(front) => front.cluster_id.as_deref(),
6703 RequestType::AddTcpFrontend(front) | RequestType::RemoveTcpFrontend(front) => {
6704 Some(&front.cluster_id)
6705 }
6706 RequestType::AddUdpFrontend(front) | RequestType::RemoveUdpFrontend(front) => {
6707 Some(&front.cluster_id)
6708 }
6709 RequestType::AddBackend(backend) | RequestType::AddUnixBackend(backend) => {
6710 Some(&backend.cluster_id)
6711 }
6712 RequestType::RemoveBackend(backend) => Some(&backend.cluster_id),
6713 _ => None,
6714 }
6715 }
6716
6717 fn assert_same_routing(actual: &ConfigState, expected: &ConfigState, context: &str) {
6719 assert_eq!(actual.clusters, expected.clusters, "{context}: clusters");
6720 assert_eq!(actual.backends, expected.backends, "{context}: backends");
6721 assert_eq!(
6722 actual.http_fronts, expected.http_fronts,
6723 "{context}: http_fronts"
6724 );
6725 assert_eq!(
6726 actual.https_fronts, expected.https_fronts,
6727 "{context}: https_fronts"
6728 );
6729 assert_eq!(
6730 actual.tcp_fronts, expected.tcp_fronts,
6731 "{context}: tcp_fronts"
6732 );
6733 assert_eq!(
6734 actual.udp_fronts, expected.udp_fronts,
6735 "{context}: udp_fronts"
6736 );
6737 }
6738
6739 #[test]
6743 fn remove_cluster_removes_its_frontends_and_backends_for_every_protocol() {
6744 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6745 let expected = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6746
6747 state
6748 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6749 .expect("removing an existing cluster must succeed");
6750
6751 assert_same_routing(&state, &expected, "after RemoveCluster(cascade_a)");
6752 assert!(
6753 state
6754 .http_fronts
6755 .values()
6756 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6757 "a route that names no cluster must survive the removal of a cluster on its hostname"
6758 );
6759
6760 let late: Request = RequestType::RemoveHttpFrontend(RequestHttpFrontend {
6764 cluster_id: Some(String::from("cascade_a")),
6765 hostname: String::from("cascade_a.test"),
6766 path: PathRule::prefix(String::from("/")),
6767 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6768 ..Default::default()
6769 })
6770 .into();
6771 let err = state
6772 .dispatch(&late)
6773 .expect_err("the cascade must already have removed the frontend");
6774 assert!(
6775 matches!(err, StateError::NotFound { .. }),
6776 "expected NotFound, got: {err}"
6777 );
6778 assert!(
6779 state.removed_with_its_cluster(&late, &err),
6780 "a removal of an object that went with its cluster must be recognised"
6781 );
6782
6783 let mut typo = cascade_deny_route();
6787 typo.cluster_id = Some(String::from("cascade_typo"));
6788 let typo: Request = RequestType::RemoveHttpFrontend(typo).into();
6789 let err = state
6790 .dispatch(&typo)
6791 .expect_err("a removal naming another cluster must not match the deny route");
6792 assert!(state.removed_with_its_cluster(&typo, &err));
6793 let answer = state.removed_with_its_cluster_message(&typo);
6794 assert!(
6795 answer.contains("this key belongs to a route with no cluster, left in place"),
6796 "the ok answer must say the deny route holds the key: {answer}"
6797 );
6798 assert!(
6799 state
6800 .http_fronts
6801 .values()
6802 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6803 "the deny route must survive a removal naming a cluster that never existed"
6804 );
6805 }
6806
6807 #[test]
6813 fn a_stale_frontend_removal_never_removes_another_clusters_route() {
6814 let route = |cluster_id: &str| RequestHttpFrontend {
6815 cluster_id: Some(cluster_id.to_owned()),
6816 hostname: String::from("reused.test"),
6817 path: PathRule::prefix(String::from("/")),
6818 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6819 ..Default::default()
6820 };
6821 let cluster = |cluster_id: &str| {
6822 RequestType::AddCluster(Cluster {
6823 cluster_id: cluster_id.to_owned(),
6824 ..Default::default()
6825 })
6826 };
6827 let owner =
6828 |state: &ConfigState, fronts: fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>| {
6829 fronts(state)
6830 .get(&route("stale_b").to_string())
6831 .and_then(|front| front.cluster_id.as_deref().map(str::to_owned))
6832 };
6833
6834 for (add, remove, fronts) in [
6835 (
6836 RequestType::AddHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6837 RequestType::RemoveHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6838 (|state: &ConfigState| &state.http_fronts)
6839 as fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>,
6840 ),
6841 (
6842 RequestType::AddHttpsFrontend,
6843 RequestType::RemoveHttpsFrontend,
6844 |state: &ConfigState| &state.https_fronts,
6845 ),
6846 ] {
6847 let mut state = ConfigState::new();
6850 for request in [
6851 cluster("stale_a"),
6852 add(route("stale_a")),
6853 RequestType::RemoveCluster(String::from("stale_a")),
6854 cluster("stale_b"),
6855 add(route("stale_b")),
6856 ] {
6857 state
6858 .dispatch(&request.clone().into())
6859 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6860 }
6861 let stale: Request = remove(route("stale_a")).into();
6862 let err = state
6863 .dispatch(&stale)
6864 .expect_err("a removal naming cluster A must not match cluster B's route");
6865 assert!(
6866 state.removed_with_its_cluster(&stale, &err),
6867 "A is gone, so its frontend went with it: {err}"
6868 );
6869 let answer = state.removed_with_its_cluster_message(&stale);
6870 assert!(
6871 answer.contains("cluster stale_a does not exist")
6872 && answer.contains("this key belongs to cluster stale_b, left in place"),
6873 "the ok answer must name the key's current owner: {answer}"
6874 );
6875 assert_eq!(
6876 owner(&state, fronts).as_deref(),
6877 Some("stale_b"),
6878 "cluster B's route must survive a stale removal naming cluster A"
6879 );
6880
6881 let mut state = ConfigState::new();
6884 for request in [
6885 cluster("stale_a"),
6886 cluster("stale_b"),
6887 add(route("stale_b")),
6888 ] {
6889 state
6890 .dispatch(&request.clone().into())
6891 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6892 }
6893 let err = state
6894 .dispatch(&stale)
6895 .expect_err("a removal naming cluster A must not match cluster B's route");
6896 assert!(
6897 !state.removed_with_its_cluster(&stale, &err),
6898 "A still exists: the removal is refused, not already done"
6899 );
6900 assert_eq!(
6901 owner(&state, fronts).as_deref(),
6902 Some("stale_b"),
6903 "cluster B's route must survive a removal naming cluster A"
6904 );
6905 }
6906 }
6907
6908 #[test]
6912 fn diff_removing_a_cluster_relies_on_the_cascade() {
6913 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6914 let after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6915
6916 let diff = before.diff(&after);
6917 assert_eq!(
6918 diff,
6919 vec![Request::from(RequestType::RemoveCluster(String::from(
6920 "cascade_a"
6921 )))],
6922 "the cascade must be the only removal the diff emits"
6923 );
6924
6925 let mut replayed = before.clone();
6926 for request in &diff {
6927 replayed
6928 .dispatch(request)
6929 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6930 }
6931 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6932 }
6933
6934 #[test]
6938 fn diff_re_adds_a_frontend_the_target_keeps_for_a_removed_cluster() {
6939 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6940 let kept = RequestHttpFrontend {
6941 cluster_id: Some(String::from("cascade_a")),
6942 hostname: String::from("cascade_a.test"),
6943 path: PathRule::prefix(String::from("/")),
6944 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6945 ..Default::default()
6946 };
6947 let mut after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6948 after
6949 .dispatch(&RequestType::AddHttpFrontend(kept.clone()).into())
6950 .expect("a frontend may name a cluster the state does not hold");
6951
6952 let diff = before.diff(&after);
6953 let removal = diff
6954 .iter()
6955 .position(|r| r.request_type == Some(RequestType::RemoveCluster("cascade_a".into())))
6956 .expect("the diff must remove cascade_a");
6957 let re_add = diff
6958 .iter()
6959 .position(|r| r.request_type == Some(RequestType::AddHttpFrontend(kept.clone())))
6960 .expect("the diff must add back the frontend the cascade removes");
6961 assert!(
6962 removal < re_add,
6963 "the frontend must be added back after its cluster is removed: {diff:?}"
6964 );
6965
6966 let mut replayed = before.clone();
6967 for request in &diff {
6968 replayed
6969 .dispatch(request)
6970 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6971 }
6972 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6973 }
6974
6975 #[test]
6979 fn a_removed_cluster_leaves_nothing_for_load_state_to_replay() {
6980 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6981 state
6982 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6983 .expect("removing an existing cluster must succeed");
6984
6985 let requests = state.generate_requests();
6986 let leaked: Vec<&Request> = requests
6987 .iter()
6988 .filter(|request| cluster_named_by(request) == Some("cascade_a"))
6989 .collect();
6990 assert!(
6991 leaked.is_empty(),
6992 "LoadState would replay objects of the removed cluster: {leaked:?}"
6993 );
6994
6995 let mut replayed = ConfigState::new();
6996 for request in &requests {
6997 replayed
6998 .dispatch(request)
6999 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
7000 }
7001 assert_same_routing(&replayed, &state, "replaying generate_requests");
7002 }
7003
7004 fn add_backend_to(cluster_id: &str, port: u16) -> Request {
7005 RequestType::AddBackend(AddBackend {
7006 cluster_id: cluster_id.to_owned(),
7007 backend_id: format!("{cluster_id}-{port}"),
7008 address: SocketAddress::new_v4(127, 0, 0, 1, port),
7009 ..Default::default()
7010 })
7011 .into()
7012 }
7013
7014 #[test]
7018 fn add_backend_for_an_unknown_cluster_is_refused() {
7019 let mut state = ConfigState::new();
7020
7021 let refused = state.dispatch(&add_backend_to("orphan", 1026));
7022 assert!(
7023 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7024 "a backend of a cluster never added must be refused, got {refused:?}"
7025 );
7026 assert!(
7027 state.backends.is_empty(),
7028 "a refused AddBackend must not leave a bucket behind: {:?}",
7029 state.backends
7030 );
7031
7032 state
7033 .dispatch(
7034 &RequestType::AddCluster(Cluster {
7035 cluster_id: String::from("orphan"),
7036 ..Default::default()
7037 })
7038 .into(),
7039 )
7040 .expect("adding the cluster must succeed");
7041 state
7042 .dispatch(&add_backend_to("orphan", 1026))
7043 .expect("a backend of an existing cluster must be added");
7044 assert_eq!(state.count_backends(), 1);
7045
7046 state
7047 .dispatch(&RequestType::RemoveCluster(String::from("orphan")).into())
7048 .expect("removing an existing cluster must succeed");
7049 assert!(
7050 state.backends.is_empty(),
7051 "RemoveCluster must remove the cluster's backends"
7052 );
7053 let refused = state.dispatch(&add_backend_to("orphan", 1027));
7054 assert!(
7055 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7056 "a backend of a removed cluster must be refused, got {refused:?}"
7057 );
7058 assert!(
7059 state.backends.is_empty(),
7060 "a backend must never outlive its cluster: {:?}",
7061 state.backends
7062 );
7063 }
7064
7065 #[test]
7069 fn add_unix_backend_for_an_unknown_cluster_is_refused() {
7070 let mut state = ConfigState::new();
7071 for unix_path in ["/run/app.sock", "relative.sock"] {
7072 let refused = state.dispatch(
7073 &RequestType::AddUnixBackend(AddBackend {
7074 cluster_id: String::from("orphan"),
7075 backend_id: String::from("orphan-unix"),
7076 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7077 unix_path: Some(unix_path.to_owned()),
7078 ..Default::default()
7079 })
7080 .into(),
7081 );
7082 assert!(
7083 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7084 "{unix_path:?}: an unknown cluster must be refused first, got {refused:?}"
7085 );
7086 assert!(
7087 state.backends.is_empty(),
7088 "a refused AddUnixBackend must not leave a bucket behind: {:?}",
7089 state.backends
7090 );
7091 }
7092 assert!(state.clusters.is_empty());
7093 }
7094
7095 #[test]
7098 fn unknown_cluster_error_names_the_object_without_the_cluster_id() {
7099 let mut state = ConfigState::new();
7100 let error = state
7101 .dispatch(&add_backend_to("secret-cluster-id", 1026))
7102 .expect_err("a backend of an unknown cluster must be refused");
7103 let message = error.to_string();
7104 assert!(
7105 !message.contains("secret-cluster-id"),
7106 "the error must not echo the cluster id: {message}"
7107 );
7108 assert!(
7109 message.contains("Backend") && message.contains("add the cluster first"),
7110 "the error must say what to do: {message}"
7111 );
7112 }
7113
7114 #[test]
7117 fn a_configuration_file_adds_each_cluster_before_its_backends() {
7118 let config = crate::config::Config::load_from_path("assets/config.toml")
7119 .expect("the test configuration must load");
7120 let mut state = ConfigState::new();
7121 for message in config
7122 .generate_config_messages()
7123 .expect("the test configuration must generate its orders")
7124 {
7125 let result = state.dispatch(&message.content);
7126 if let Some(RequestType::AddBackend(_) | RequestType::AddCluster(_)) =
7127 &message.content.request_type
7128 {
7129 result.unwrap_or_else(|e| panic!("could not apply {message:?}: {e}"));
7130 }
7131 }
7132 assert_eq!(state.clusters.len(), 3);
7133 assert_eq!(state.count_backends(), 5);
7134 }
7135
7136 #[test]
7139 fn diff_adds_a_cluster_before_its_backends() {
7140 let target = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
7141 let mut state = ConfigState::new();
7142 for request in state.diff(&target) {
7143 state
7144 .dispatch(&request)
7145 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
7146 }
7147 assert_same_routing(&state, &target, "replaying diff(empty, target)");
7148 }
7149
7150 fn app_cluster_probing(port: Option<u32>, unix_path: Option<&str>) -> ConfigState {
7153 let mut state = ConfigState::new();
7154 let health_check = crate::proto::command::HealthCheckConfig {
7155 interval: 10,
7156 timeout: 5,
7157 healthy_threshold: 3,
7158 unhealthy_threshold: 3,
7159 mode: crate::proto::command::HealthCheckMode::Tcp as i32,
7160 port,
7161 ..Default::default()
7162 };
7163 state
7164 .dispatch(
7165 &RequestType::AddCluster(Cluster {
7166 cluster_id: String::from("app"),
7167 health_check: Some(health_check),
7168 ..Default::default()
7169 })
7170 .into(),
7171 )
7172 .expect("the cluster must be accepted");
7173 state
7174 .dispatch(
7175 &RequestType::add_backend(AddBackend {
7176 cluster_id: String::from("app"),
7177 backend_id: String::from("app-0"),
7178 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
7179 unix_path: unix_path.map(ToOwned::to_owned),
7180 ..Default::default()
7181 })
7182 .into(),
7183 )
7184 .expect("the backend must be accepted");
7185 state
7186 }
7187
7188 #[test]
7194 fn diff_converges_when_a_probe_port_and_a_unix_backend_swap() {
7195 let unix = app_cluster_probing(None, Some("/run/app.sock"));
7196 let ip = app_cluster_probing(Some(8081), None);
7197 for (from, to, direction) in [
7198 (&unix, &ip, "unix -> ip+port"),
7199 (&ip, &unix, "ip+port -> unix"),
7200 ] {
7201 let mut state = from.clone();
7202 for request in from.diff(to) {
7203 state
7204 .dispatch(&request)
7205 .unwrap_or_else(|e| panic!("{direction}: could not apply {request:?}: {e}"));
7206 }
7207 assert_same_routing(&state, to, direction);
7208 }
7209 }
7210
7211 #[test]
7212 fn cluster_ids_by_domain() {
7213 let mut config = ConfigState::new();
7214 let http_front_cluster1 = RequestHttpFrontend {
7215 cluster_id: Some(String::from("MyCluster_1")),
7216 hostname: String::from("lolcatho.st"),
7217 path: PathRule::prefix(String::from("")),
7218 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7219 ..Default::default()
7220 };
7221
7222 let https_front_cluster1 = RequestHttpFrontend {
7223 cluster_id: Some(String::from("MyCluster_1")),
7224 hostname: String::from("lolcatho.st"),
7225 path: PathRule::prefix(String::from("")),
7226 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7227 ..Default::default()
7228 };
7229
7230 let http_front_cluster2 = RequestHttpFrontend {
7231 cluster_id: Some(String::from("MyCluster_2")),
7232 hostname: String::from("lolcatho.st"),
7233 path: PathRule::prefix(String::from("/api")),
7234 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7235 ..Default::default()
7236 };
7237
7238 let https_front_cluster2 = RequestHttpFrontend {
7239 cluster_id: Some(String::from("MyCluster_2")),
7240 hostname: String::from("lolcatho.st"),
7241 path: PathRule::prefix(String::from("/api")),
7242 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7243 ..Default::default()
7244 };
7245
7246 config
7247 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster1).into())
7248 .expect("Could not execute request");
7249 config
7250 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster2).into())
7251 .expect("Could not execute request");
7252 config
7253 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster1).into())
7254 .expect("Could not execute request");
7255 config
7256 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster2).into())
7257 .expect("Could not execute request");
7258
7259 let mut cluster1_cluster2: HashSet<ClusterId> = HashSet::new();
7260 cluster1_cluster2.insert(ClusterId::from("MyCluster_1"));
7261 cluster1_cluster2.insert(ClusterId::from("MyCluster_2"));
7262
7263 let mut cluster2: HashSet<ClusterId> = HashSet::new();
7264 cluster2.insert(ClusterId::from("MyCluster_2"));
7265
7266 let empty: HashSet<ClusterId> = HashSet::new();
7267 assert_eq!(
7268 config.get_cluster_ids_by_domain(String::from("lolcatho.st"), None),
7269 cluster1_cluster2
7270 );
7271 assert_eq!(
7272 config
7273 .get_cluster_ids_by_domain(String::from("lolcatho.st"), Some(String::from("/api"))),
7274 cluster2
7275 );
7276 assert_eq!(
7277 config.get_cluster_ids_by_domain(String::from("lolcathost"), None),
7278 empty
7279 );
7280 assert_eq!(
7281 config
7282 .get_cluster_ids_by_domain(String::from("lolcathost"), Some(String::from("/sozu"))),
7283 empty
7284 );
7285 }
7286
7287 #[test]
7288 fn duplicate_backends() {
7289 let mut state: ConfigState = Default::default();
7290 add_clusters(&mut state, &["cluster_1"]);
7291 state
7292 .dispatch(
7293 &RequestType::AddBackend(AddBackend {
7294 cluster_id: String::from("cluster_1"),
7295 backend_id: String::from("cluster_1-0"),
7296 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7297 load_balancing_parameters: Some(LoadBalancingParams::default()),
7298 ..Default::default()
7299 })
7300 .into(),
7301 )
7302 .expect("Could not execute request");
7303
7304 let b = Backend {
7305 cluster_id: ClusterId::from("cluster_1"),
7306 backend_id: String::from("cluster_1-0"),
7307 address: "127.0.0.1:1026".parse().unwrap(),
7308 load_balancing_parameters: Some(LoadBalancingParams::default()),
7309 sticky_id: Some("sticky".to_string()),
7310 backup: None,
7311 unix_path: None,
7312 };
7313
7314 state
7315 .dispatch(&RequestType::AddBackend(b.clone().to_add_backend()).into())
7316 .expect("Could not execute order");
7317
7318 assert_eq!(state.backends.get("cluster_1").unwrap(), &vec![b]);
7319 }
7320
7321 #[test]
7325 fn a_unix_path_backend_round_trips_through_the_state() {
7326 let mut state: ConfigState = Default::default();
7327 add_clusters(&mut state, &["cluster_1"]);
7328 let add = AddBackend {
7329 cluster_id: String::from("cluster_1"),
7330 backend_id: String::from("cluster_1-unix"),
7331 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7332 unix_path: Some(String::from("/run/app.sock")),
7333 ..Default::default()
7334 };
7335 state
7336 .dispatch(&RequestType::AddBackend(add.clone()).into())
7337 .expect("a valid unix_path is accepted");
7338 assert_eq!(
7339 state.backends["cluster_1"][0].unix_path.as_deref(),
7340 Some("/run/app.sock")
7341 );
7342 let replayed: Vec<AddBackend> = state
7343 .generate_requests()
7344 .into_iter()
7345 .filter_map(|request| match request.request_type {
7346 Some(RequestType::AddUnixBackend(backend)) => Some(backend),
7347 _ => None,
7348 })
7349 .collect();
7350 assert_eq!(replayed, vec![add]);
7351 }
7352
7353 #[test]
7356 fn an_invalid_unix_path_is_refused() {
7357 let mut state: ConfigState = Default::default();
7358 add_clusters(&mut state, &["cluster_1"]);
7359 for refused in ["", "relative.sock", "@"] {
7360 let result = state.dispatch(
7361 &RequestType::AddBackend(AddBackend {
7362 cluster_id: String::from("cluster_1"),
7363 backend_id: String::from("cluster_1-unix"),
7364 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7365 unix_path: Some(refused.to_owned()),
7366 ..Default::default()
7367 })
7368 .into(),
7369 );
7370 assert!(
7371 matches!(
7372 result,
7373 Err(StateError::InvalidValue {
7374 field: "unix_path",
7375 ..
7376 })
7377 ),
7378 "{refused:?} must be refused"
7379 );
7380 }
7381 assert!(state.backends.is_empty());
7382 }
7383
7384 #[test]
7385 fn remove_backend() {
7386 let mut state: ConfigState = Default::default();
7387 state
7388 .dispatch(
7389 &RequestType::AddCluster(Cluster {
7390 cluster_id: String::from("cluster_1"),
7391 ..Default::default()
7392 })
7393 .into(),
7394 )
7395 .expect("Could not execute request");
7396
7397 for i in 0..10 {
7398 state
7399 .dispatch(
7400 &RequestType::AddBackend(AddBackend {
7401 cluster_id: String::from("cluster_1"),
7402 backend_id: format!("cluster_1-{i}"),
7403 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7404 ..Default::default()
7405 })
7406 .into(),
7407 )
7408 .expect("Could not execute request");
7409 }
7410
7411 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 10);
7412
7413 let remove_backend_2 = RequestType::RemoveBackend(RemoveBackend {
7414 cluster_id: String::from("cluster_1"),
7415 backend_id: String::from("cluster_1-0"),
7416 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7417 })
7418 .into();
7419
7420 let remove_backend_result = state.dispatch(&remove_backend_2);
7421
7422 assert!(remove_backend_result.is_ok());
7423 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7424
7425 let redundant_remove = state.dispatch(&remove_backend_2);
7426 assert!(matches!(redundant_remove, Err(StateError::NoChange)));
7427 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7428 }
7429
7430 #[test]
7431 fn remove_backends_randomly() {
7432 let mut state: ConfigState = Default::default();
7433 state
7434 .dispatch(
7435 &RequestType::AddCluster(Cluster {
7436 cluster_id: String::from("cluster_1"),
7437 ..Default::default()
7438 })
7439 .into(),
7440 )
7441 .expect("Could not execute request");
7442
7443 for _ in 0..1000 {
7444 for i in 0..10 {
7445 state
7446 .dispatch(
7447 &RequestType::AddBackend(AddBackend {
7448 cluster_id: String::from("cluster_1"),
7449 backend_id: format!("cluster_1-{i}"),
7450 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7451 ..Default::default()
7452 })
7453 .into(),
7454 )
7455 .expect("Could not execute request");
7456 }
7457
7458 let mut rng = rng();
7459 let mut indexes = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
7460 indexes.shuffle(&mut rng);
7461 let random_count = rng.random_range(1..indexes.len());
7462 let random_indexes: Vec<i32> = indexes.into_iter().take(random_count).collect();
7463
7464 for j in random_indexes {
7465 let remove_backend_result = state.dispatch(
7466 &RequestType::RemoveBackend(RemoveBackend {
7467 cluster_id: String::from("cluster_1"),
7468 backend_id: format!("cluster_1-{j}"),
7469 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7470 })
7471 .into(),
7472 );
7473 assert!(remove_backend_result.is_ok());
7474 }
7475 }
7476 }
7477
7478 #[test]
7479 fn listener_diff() {
7480 let mut state: ConfigState = Default::default();
7481 let custom_http_answers = Some(CustomHttpAnswers {
7482 answer_404: Some("test".to_string()),
7483 ..Default::default()
7484 });
7485 state
7486 .dispatch(
7487 &RequestType::AddTcpListener(TcpListenerConfig {
7488 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7489 ..Default::default()
7490 })
7491 .into(),
7492 )
7493 .expect("Could not execute request");
7494 state
7495 .dispatch(
7496 &RequestType::ActivateListener(ActivateListener {
7497 interface: None,
7498 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7499 proxy: ListenerType::Tcp.into(),
7500 from_scm: false,
7501 })
7502 .into(),
7503 )
7504 .expect("Could not execute request");
7505 state
7506 .dispatch(
7507 &RequestType::AddHttpListener(HttpListenerConfig {
7508 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7509 ..Default::default()
7510 })
7511 .into(),
7512 )
7513 .expect("Could not execute request");
7514 state
7515 .dispatch(
7516 &RequestType::AddHttpsListener(HttpsListenerConfig {
7517 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7518 ..Default::default()
7519 })
7520 .into(),
7521 )
7522 .expect("Could not execute request");
7523 state
7524 .dispatch(
7525 &RequestType::ActivateListener(ActivateListener {
7526 interface: None,
7527 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7528 proxy: ListenerType::Https.into(),
7529 from_scm: false,
7530 })
7531 .into(),
7532 )
7533 .expect("Could not execute request");
7534
7535 let mut state2: ConfigState = Default::default();
7536 state2
7537 .dispatch(
7538 &RequestType::AddTcpListener(TcpListenerConfig {
7539 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7540 expect_proxy: true,
7541 ..Default::default()
7542 })
7543 .into(),
7544 )
7545 .expect("Could not execute request");
7546 state2
7547 .dispatch(
7548 &RequestType::AddHttpListener(HttpListenerConfig {
7549 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7550 http_answers: custom_http_answers.clone(),
7551 ..Default::default()
7552 })
7553 .into(),
7554 )
7555 .expect("Could not execute request");
7556 state2
7557 .dispatch(
7558 &RequestType::ActivateListener(ActivateListener {
7559 interface: None,
7560 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7561 proxy: ListenerType::Http.into(),
7562 from_scm: false,
7563 })
7564 .into(),
7565 )
7566 .expect("Could not execute request");
7567 state2
7568 .dispatch(
7569 &RequestType::AddHttpsListener(HttpsListenerConfig {
7570 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7571 http_answers: custom_http_answers.clone(),
7572 ..Default::default()
7573 })
7574 .into(),
7575 )
7576 .expect("Could not execute request");
7577 state2
7578 .dispatch(
7579 &RequestType::ActivateListener(ActivateListener {
7580 interface: None,
7581 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7582 proxy: ListenerType::Https.into(),
7583 from_scm: false,
7584 })
7585 .into(),
7586 )
7587 .expect("Could not execute request");
7588
7589 let e: Vec<Request> = vec![
7590 RequestType::RemoveListener(RemoveListener {
7591 interface: None,
7592 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7593 proxy: ListenerType::Tcp.into(),
7594 })
7595 .into(),
7596 RequestType::AddTcpListener(TcpListenerConfig {
7597 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7598 expect_proxy: true,
7599 ..Default::default()
7600 })
7601 .into(),
7602 RequestType::DeactivateListener(DeactivateListener {
7603 interface: None,
7604 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7605 proxy: ListenerType::Tcp.into(),
7606 to_scm: false,
7607 })
7608 .into(),
7609 RequestType::RemoveListener(RemoveListener {
7610 interface: None,
7611 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7612 proxy: ListenerType::Http.into(),
7613 })
7614 .into(),
7615 RequestType::AddHttpListener(HttpListenerConfig {
7616 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7617 http_answers: custom_http_answers.clone(),
7618 ..Default::default()
7619 })
7620 .into(),
7621 RequestType::ActivateListener(ActivateListener {
7622 interface: None,
7623 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7624 proxy: ListenerType::Http.into(),
7625 from_scm: false,
7626 })
7627 .into(),
7628 RequestType::RemoveListener(RemoveListener {
7629 interface: None,
7630 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7631 proxy: ListenerType::Https.into(),
7632 })
7633 .into(),
7634 RequestType::AddHttpsListener(HttpsListenerConfig {
7635 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7636 http_answers: custom_http_answers.clone(),
7637 ..Default::default()
7638 })
7639 .into(),
7640 RequestType::ActivateListener(ActivateListener {
7646 interface: None,
7647 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7648 proxy: ListenerType::Https.into(),
7649 from_scm: false,
7650 })
7651 .into(),
7652 ];
7653
7654 let diff = state.diff(&state2);
7655 println!("expected diff requests:\n{e:#?}\n");
7657 println!("diff requests:\n{diff:#?}\n");
7658
7659 let _hash1 = state.hash_state();
7660 let _hash2 = state2.hash_state();
7661
7662 assert_eq!(diff, e);
7663
7664 let mut replayed = state.clone();
7671 for request in &diff {
7672 replayed
7673 .dispatch(request)
7674 .expect("every diff request must replay cleanly onto the source state");
7675 }
7676 assert_eq!(
7677 replayed.tcp_listeners, state2.tcp_listeners,
7678 "replayed tcp_listeners must match the target state"
7679 );
7680 assert_eq!(
7681 replayed.http_listeners, state2.http_listeners,
7682 "replayed http_listeners must match the target state"
7683 );
7684 assert_eq!(
7685 replayed.https_listeners, state2.https_listeners,
7686 "replayed https_listeners must match the target state"
7687 );
7688 let replayed_8443 = replayed
7691 .https_listeners
7692 .get(&ListenerKey::from(SocketAddress::new_v4(0, 0, 0, 0, 8443)))
7693 .expect("8443 HTTPS listener must exist after replay");
7694 assert!(
7695 replayed_8443.active,
7696 "the 8443 HTTPS listener must stay ACTIVE across a config change"
7697 );
7698 }
7699
7700 #[test]
7701 fn certificate_retrieval() {
7702 let mut state: ConfigState = Default::default();
7703 let certificate_and_key = CertificateAndKey {
7704 certificate: String::from(include_str!("../assets/certificate.pem")),
7705 key: String::from(include_str!("../assets/key.pem")),
7706 certificate_chain: vec![],
7707 versions: vec![],
7708 names: vec!["lolcatho.st".to_string()],
7709 };
7710 let add_certificate = AddCertificate {
7711 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
7712 certificate: certificate_and_key,
7713 expired_at: None,
7714 };
7715 state
7716 .dispatch(&RequestType::AddCertificate(add_certificate).into())
7717 .expect("Could not add certificate");
7718
7719 println!("state: {state:#?}");
7720
7721 let certificates_found_by_fingerprint = state.get_certificates(QueryCertificatesFilters {
7727 domain: None,
7728 fingerprint: Some(
7729 "ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5".to_string(),
7730 ),
7731 });
7732
7733 println!("found certificate: {certificates_found_by_fingerprint:#?}");
7734
7735 assert!(!certificates_found_by_fingerprint.is_empty());
7736
7737 let certificate_found_by_domain_name = state.get_certificates(QueryCertificatesFilters {
7738 domain: Some("lolcatho.st".to_string()),
7739 fingerprint: None,
7740 });
7741
7742 assert!(!certificate_found_by_domain_name.is_empty());
7743 }
7744
7745 #[test]
7746 fn count_backends_across_clusters() {
7747 let mut state: ConfigState = Default::default();
7748 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
7749
7750 assert_eq!(state.count_backends(), 0);
7751
7752 state
7753 .dispatch(
7754 &RequestType::AddBackend(AddBackend {
7755 cluster_id: String::from("cluster_1"),
7756 backend_id: String::from("cluster_1-0"),
7757 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7758 ..Default::default()
7759 })
7760 .into(),
7761 )
7762 .expect("Could not execute request");
7763 assert_eq!(state.count_backends(), 1);
7764
7765 state
7766 .dispatch(
7767 &RequestType::AddBackend(AddBackend {
7768 cluster_id: String::from("cluster_1"),
7769 backend_id: String::from("cluster_1-1"),
7770 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
7771 ..Default::default()
7772 })
7773 .into(),
7774 )
7775 .expect("Could not execute request");
7776 assert_eq!(state.count_backends(), 2);
7777
7778 state
7780 .dispatch(
7781 &RequestType::AddBackend(AddBackend {
7782 cluster_id: String::from("cluster_2"),
7783 backend_id: String::from("cluster_2-0"),
7784 address: SocketAddress::new_v4(192, 168, 1, 1, 8080),
7785 ..Default::default()
7786 })
7787 .into(),
7788 )
7789 .expect("Could not execute request");
7790 assert_eq!(state.count_backends(), 3);
7791
7792 state
7794 .dispatch(
7795 &RequestType::RemoveBackend(RemoveBackend {
7796 cluster_id: String::from("cluster_1"),
7797 backend_id: String::from("cluster_1-0"),
7798 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7799 })
7800 .into(),
7801 )
7802 .expect("Could not execute request");
7803 assert_eq!(state.count_backends(), 2);
7804 }
7805
7806 #[test]
7836 fn a_regex_hostname_is_always_replayed_before_the_exact_name_it_matches() {
7837 let mut state: ConfigState = Default::default();
7838
7839 for hostname in ["test4.example.com", "/test[0-9]/.example.com"] {
7842 state
7843 .dispatch(
7844 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7845 cluster_id: Some(String::from("cluster_1")),
7846 hostname: String::from(hostname),
7847 path: PathRule::prefix(String::from("/")),
7848 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7849 position: RulePosition::Tree.into(),
7850 ..Default::default()
7851 })
7852 .into(),
7853 )
7854 .expect("Could not execute request");
7855 }
7856
7857 let replayed: Vec<String> = state
7860 .produce_initial_state()
7861 .requests
7862 .into_iter()
7863 .filter_map(|worker_request| match worker_request.content.request_type {
7864 Some(RequestType::AddHttpFrontend(front)) => Some(front.hostname),
7865 _ => None,
7866 })
7867 .collect();
7868
7869 assert_eq!(
7870 replayed,
7871 vec![
7872 String::from("/test[0-9]/.example.com"),
7873 String::from("test4.example.com"),
7874 ],
7875 "a worker replays the regex segment before the exact name it \
7876 matches, whatever order the operator added them in",
7877 );
7878 }
7879
7880 #[test]
7881 fn count_frontends_across_types() {
7882 let mut state: ConfigState = Default::default();
7883
7884 assert_eq!(state.count_frontends(), 0);
7885
7886 state
7888 .dispatch(
7889 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7890 cluster_id: Some(String::from("cluster_1")),
7891 hostname: String::from("example.com"),
7892 path: PathRule::prefix(String::from("/")),
7893 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7894 position: RulePosition::Tree.into(),
7895 ..Default::default()
7896 })
7897 .into(),
7898 )
7899 .expect("Could not execute request");
7900 assert_eq!(state.count_frontends(), 1);
7901
7902 state
7904 .dispatch(
7905 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
7906 cluster_id: Some(String::from("cluster_1")),
7907 hostname: String::from("secure.example.com"),
7908 path: PathRule::prefix(String::from("/")),
7909 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7910 position: RulePosition::Tree.into(),
7911 ..Default::default()
7912 })
7913 .into(),
7914 )
7915 .expect("Could not execute request");
7916 assert_eq!(state.count_frontends(), 2);
7917
7918 state
7920 .dispatch(
7921 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7922 cluster_id: String::from("cluster_2"),
7923 address: SocketAddress::new_v4(0, 0, 0, 0, 5432),
7924 ..Default::default()
7925 })
7926 .into(),
7927 )
7928 .expect("Could not execute request");
7929 assert_eq!(state.count_frontends(), 3);
7930
7931 state
7933 .dispatch(
7934 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7935 cluster_id: String::from("cluster_2"),
7936 address: SocketAddress::new_v4(0, 0, 0, 0, 5433),
7937 ..Default::default()
7938 })
7939 .into(),
7940 )
7941 .expect("Could not execute request");
7942 assert_eq!(state.count_frontends(), 4);
7943
7944 state
7946 .dispatch(
7947 &RequestType::RemoveHttpFrontend(RequestHttpFrontend {
7948 cluster_id: Some(String::from("cluster_1")),
7949 hostname: String::from("example.com"),
7950 path: PathRule::prefix(String::from("/")),
7951 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7952 position: RulePosition::Tree.into(),
7953 ..Default::default()
7954 })
7955 .into(),
7956 )
7957 .expect("Could not execute request");
7958 assert_eq!(state.count_frontends(), 3);
7959 }
7960
7961 #[test]
7967 fn https_listener_requests_carry_client_auth_on_their_own_verb() {
7968 let mtls_address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
7969 let plain_address = SocketAddress::new_v4(127, 0, 0, 1, 8444);
7970 let mtls = HttpsListenerConfig {
7971 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
7972 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
7973 ..make_https_listener(mtls_address)
7974 };
7975
7976 let mut state = ConfigState::new();
7977 state
7978 .dispatch(&RequestType::AddHttpsListenerWithClientAuth(mtls.clone()).into())
7979 .expect("the mTLS verb must add the listener to the state");
7980 state
7981 .dispatch(&RequestType::AddHttpsListener(make_https_listener(plain_address)).into())
7982 .expect("the historical verb must keep adding listeners without client auth");
7983 assert_eq!(
7984 state.https_listeners.len(),
7985 2,
7986 "both verbs add a listener to the same state"
7987 );
7988
7989 let https_adds = |requests: Vec<Request>| -> Vec<(SocketAddress, bool)> {
7990 requests
7991 .into_iter()
7992 .filter_map(|request| match request.request_type {
7993 Some(RequestType::AddHttpsListener(listener)) => {
7994 Some((listener.address, false))
7995 }
7996 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => {
7997 Some((listener.address, true))
7998 }
7999 _ => None,
8000 })
8001 .collect()
8002 };
8003 let mut generated = https_adds(state.generate_requests());
8004 generated.sort_by_key(|(address, _)| address.port);
8005 assert_eq!(
8006 generated,
8007 vec![(mtls_address, true), (plain_address, false)],
8008 "generate_requests must put only the listener asking for a client certificate on the mTLS verb"
8009 );
8010
8011 let diff = https_adds(ConfigState::new().diff(&state));
8012 assert!(
8013 diff.contains(&(mtls_address, true)) && !diff.contains(&(mtls_address, false)),
8014 "the reload diff must add the mTLS listener on the mTLS verb: {diff:?}"
8015 );
8016 }
8017
8018 #[test]
8023 fn a_reload_refreshes_the_client_auth_policy_of_a_running_listener() {
8024 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
8025 let with_crl = |crl: &str| HttpsListenerConfig {
8026 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
8027 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
8028 client_ca_crls: vec![crl.to_owned()],
8029 ..make_https_listener(address)
8030 };
8031
8032 let mut state = ConfigState::new();
8033 assert_eq!(
8034 state.client_auth_refresh(&with_crl("CRL 1")),
8035 None,
8036 "a listener the state does not hold is added, not patched"
8037 );
8038 state
8039 .dispatch(&RequestType::add_https_listener(with_crl("CRL 1")).into())
8040 .expect("add the listener");
8041 assert_eq!(
8042 state.client_auth_refresh(&with_crl("CRL 1")),
8043 None,
8044 "an unchanged policy needs no patch"
8045 );
8046 assert!(
8047 matches!(
8048 state.dispatch(&RequestType::add_https_listener(with_crl("CRL 2")).into()),
8049 Err(StateError::Exists { .. })
8050 ),
8051 "precondition: replaying the add for a held address is refused"
8052 );
8053
8054 let patch = state
8055 .client_auth_refresh(&with_crl("CRL 2"))
8056 .expect("a refreshed CRL must produce a patch");
8057 assert_eq!(patch.address, address);
8058 assert_eq!(
8059 patch.client_auth_policy,
8060 Some(with_crl("CRL 2").client_auth_policy())
8061 );
8062 assert_eq!(
8063 UpdateHttpsListenerConfig {
8064 client_auth_policy: None,
8065 ..patch.clone()
8066 },
8067 UpdateHttpsListenerConfig {
8068 address,
8069 ..Default::default()
8070 },
8071 "the patch carries the policy and nothing else"
8072 );
8073
8074 state
8075 .dispatch(&RequestType::update_https_listener(patch).into())
8076 .expect("the state must apply the policy patch");
8077 let running = state
8078 .https_listeners
8079 .values()
8080 .next()
8081 .expect("the listener is still there");
8082 assert_eq!(running.client_ca_crls, vec!["CRL 2".to_owned()]);
8083 assert_eq!(
8084 state.client_auth_refresh(&with_crl("CRL 2")),
8085 None,
8086 "once applied, the same reload has nothing left to refresh"
8087 );
8088 }
8089
8090 fn make_https_listener(address: SocketAddress) -> HttpsListenerConfig {
8093 HttpsListenerConfig {
8094 address,
8095 sticky_name: "SOZUBALANCEID".to_owned(),
8096 front_timeout: 60,
8097 back_timeout: 30,
8098 connect_timeout: 3,
8099 request_timeout: 10,
8100 ..Default::default()
8101 }
8102 }
8103
8104 fn make_http_listener(address: SocketAddress) -> HttpListenerConfig {
8105 HttpListenerConfig {
8106 address,
8107 sticky_name: "SOZUBALANCEID".to_owned(),
8108 front_timeout: 60,
8109 back_timeout: 30,
8110 connect_timeout: 3,
8111 request_timeout: 10,
8112 ..Default::default()
8113 }
8114 }
8115
8116 fn make_tcp_listener(address: SocketAddress) -> TcpListenerConfig {
8117 TcpListenerConfig {
8118 address,
8119 front_timeout: 60,
8120 back_timeout: 30,
8121 connect_timeout: 3,
8122 ..Default::default()
8123 }
8124 }
8125
8126 fn make_udp_listener(address: SocketAddress, active: bool) -> UdpListenerConfig {
8127 UdpListenerConfig {
8128 interface: None,
8129 address,
8130 public_address: None,
8131 front_timeout: 30,
8132 back_timeout: 30,
8133 max_rx_datagram_size: 1500,
8134 max_flows: 0,
8135 active,
8136 }
8137 }
8138
8139 #[test]
8144 fn activation_rollback_restores_the_prior_flag_for_every_listener_type() {
8145 let mut state = ConfigState::new();
8146 let cases = [
8147 (ListenerType::Http, 8200),
8148 (ListenerType::Https, 8201),
8149 (ListenerType::Tcp, 8202),
8150 (ListenerType::Udp, 8203),
8151 ];
8152
8153 for (listener_type, port) in cases {
8154 let address = SocketAddress::new_v4(127, 0, 0, 1, port);
8155 let interface = Some(format!("test-{port}"));
8156 let proxy: i32 = listener_type.into();
8157 let add = match listener_type {
8158 ListenerType::Http => RequestType::AddHttpListener(with_interface(
8159 make_http_listener(address),
8160 |listener| listener.interface = interface.to_owned(),
8161 )),
8162 ListenerType::Https => RequestType::AddHttpsListener(with_interface(
8163 make_https_listener(address),
8164 |listener| listener.interface = interface.to_owned(),
8165 )),
8166 ListenerType::Tcp => RequestType::AddTcpListener(with_interface(
8167 make_tcp_listener(address),
8168 |listener| listener.interface = interface.to_owned(),
8169 )),
8170 ListenerType::Udp => RequestType::AddUdpListener(with_interface(
8171 make_udp_listener(address, false),
8172 |listener| listener.interface = interface.to_owned(),
8173 )),
8174 };
8175 state
8176 .dispatch(&add.into())
8177 .expect("the inactive listener must be recorded");
8178
8179 let activate = ActivateListener {
8180 address,
8181 proxy,
8182 from_scm: false,
8183 interface: interface.to_owned(),
8184 };
8185 let rollback = state
8186 .rollback_for(&RequestType::ActivateListener(activate.to_owned()))
8187 .expect("an inactive listener must receive an activation rollback");
8188 assert!(matches!(
8189 rollback.request_type,
8190 Some(RequestType::DeactivateListener(DeactivateListener {
8191 address: rollback_address,
8192 proxy,
8193 to_scm: false,
8194 interface: rollback_interface,
8195 })) if rollback_address == address
8196 && proxy == activate.proxy
8197 && rollback_interface == interface
8198 ));
8199
8200 state
8201 .dispatch(&RequestType::ActivateListener(activate.to_owned()).into())
8202 .expect("the listener must become active in the stored state");
8203 assert!(
8204 state
8205 .rollback_for(&RequestType::ActivateListener(activate))
8206 .is_none(),
8207 "repeating activation for an already-active {listener_type:?} listener must not create a deactivation rollback"
8208 );
8209 }
8210 }
8211
8212 #[test]
8222 fn test_udp_state_roundtrip() {
8223 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
8224
8225 let mut state = ConfigState::default();
8226 state
8227 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
8228 .expect("could not add udp listener");
8229 state
8230 .dispatch(
8231 &RequestType::ActivateListener(ActivateListener {
8232 interface: None,
8233 address,
8234 proxy: ListenerType::Udp.into(),
8235 from_scm: false,
8236 })
8237 .into(),
8238 )
8239 .expect("could not activate udp listener");
8240 state
8241 .dispatch(
8242 &RequestType::AddUdpFrontend(RequestUdpFrontend {
8243 cluster_id: "udp_cluster".to_string(),
8244 address,
8245 tags: BTreeMap::from([("owner".to_string(), "team".to_string())]),
8246 })
8247 .into(),
8248 )
8249 .expect("could not add udp frontend");
8250
8251 assert_eq!(state.udp_listeners.len(), 1);
8252 assert!(state.udp_listeners[&address.into()].active);
8253 assert_eq!(
8254 state.udp_fronts.get("udp_cluster").map(Vec::len),
8255 Some(1usize)
8256 );
8257
8258 let logical = |s: &ConfigState| {
8263 let mut c = s.clone();
8264 c.request_counts.clear();
8265 c
8266 };
8267
8268 let mut replayed = ConfigState::default();
8270 for request in state.generate_requests() {
8271 replayed
8272 .dispatch(&request)
8273 .expect("could not replay generated request");
8274 }
8275 assert_eq!(
8276 logical(&state),
8277 logical(&replayed),
8278 "UDP listener + frontend must survive generate_requests → replay"
8279 );
8280
8281 let empty = ConfigState::default();
8283 let mut from_diff = ConfigState::default();
8284 for request in empty.diff(&state) {
8285 from_diff
8286 .dispatch(&request)
8287 .expect("could not replay diff request");
8288 }
8289 assert_eq!(
8290 logical(&state),
8291 logical(&from_diff),
8292 "diff(empty -> state) must reconstruct the UDP listener + frontend"
8293 );
8294
8295 let mut torn_down = state.clone();
8297 for request in state.diff(&empty) {
8298 torn_down
8299 .dispatch(&request)
8300 .expect("could not replay teardown diff request");
8301 }
8302 assert!(
8303 torn_down.udp_listeners.is_empty(),
8304 "diff(state -> empty) must remove the UDP listener"
8305 );
8306 assert!(
8307 torn_down
8308 .udp_fronts
8309 .get("udp_cluster")
8310 .map(Vec::is_empty)
8311 .unwrap_or(true),
8312 "diff(state -> empty) must remove the UDP frontend"
8313 );
8314
8315 state
8317 .dispatch(
8318 &RequestType::UpdateUdpListener(UpdateUdpListenerConfig {
8319 address,
8320 max_flows: Some(4096),
8321 front_timeout: Some(15),
8322 ..Default::default()
8323 })
8324 .into(),
8325 )
8326 .expect("could not update udp listener");
8327 let updated = &state.udp_listeners[&address.into()];
8328 assert_eq!(updated.max_flows, 4096);
8329 assert_eq!(updated.front_timeout, 15);
8330 assert_eq!(
8331 updated.back_timeout, 30,
8332 "unpatched field must be preserved"
8333 );
8334 }
8335
8336 #[test]
8351 fn test_tcp_sni_alpn_state_roundtrip() {
8352 let address = SocketAddress::new_v4(127, 0, 0, 1, 6443);
8353
8354 let mut state = ConfigState::default();
8355 state
8356 .dispatch(
8357 &RequestType::AddTcpListener(TcpListenerConfig {
8358 address,
8359 front_timeout: 60,
8360 back_timeout: 30,
8361 connect_timeout: 3,
8362 active: true,
8363 sni_preread_timeout: Some(2),
8364 sni_preread_max_bytes: Some(8192),
8365 ..Default::default()
8366 })
8367 .into(),
8368 )
8369 .expect("could not add tcp listener");
8370 state
8371 .dispatch(
8372 &RequestType::ActivateListener(ActivateListener {
8373 interface: None,
8374 address,
8375 proxy: ListenerType::Tcp.into(),
8376 from_scm: false,
8377 })
8378 .into(),
8379 )
8380 .expect("could not activate tcp listener");
8381
8382 let frontend_a = RequestTcpFrontend {
8383 cluster_id: "cluster_sni_a".to_string(),
8384 address,
8385 tags: BTreeMap::new(),
8386 sni: Some("example.com".to_string()),
8387 alpn: vec!["h2".to_string()],
8388 };
8389 let frontend_b = RequestTcpFrontend {
8390 cluster_id: "cluster_sni_b".to_string(),
8391 address,
8392 tags: BTreeMap::new(),
8393 sni: Some("other.example.com".to_string()),
8394 alpn: vec![],
8395 };
8396 state
8397 .dispatch(&RequestType::AddTcpFrontend(frontend_a.clone()).into())
8398 .expect("could not add sni-a tcp frontend");
8399 state
8400 .dispatch(&RequestType::AddTcpFrontend(frontend_b.clone()).into())
8401 .expect("could not add sni-b tcp frontend");
8402
8403 assert_eq!(state.tcp_listeners.len(), 1);
8404 assert!(state.tcp_listeners[&address.into()].active);
8405 assert_eq!(
8406 state.tcp_listeners[&address.into()].sni_preread_timeout,
8407 Some(2)
8408 );
8409 assert_eq!(
8410 state.tcp_listeners[&address.into()].sni_preread_max_bytes,
8411 Some(8192)
8412 );
8413 assert_eq!(state.count_tcp_frontends_raw(), 2);
8414
8415 let logical = |s: &ConfigState| {
8420 let mut c = s.clone();
8421 c.request_counts.clear();
8422 c
8423 };
8424
8425 let mut replayed = ConfigState::default();
8427 for request in state.generate_requests() {
8428 replayed
8429 .dispatch(&request)
8430 .expect("could not replay generated request");
8431 }
8432 assert_eq!(
8433 logical(&state),
8434 logical(&replayed),
8435 "SNI/ALPN TCP listener + frontends must survive generate_requests → replay"
8436 );
8437
8438 let empty = ConfigState::default();
8440 let mut from_diff = ConfigState::default();
8441 for request in empty.diff(&state) {
8442 from_diff
8443 .dispatch(&request)
8444 .expect("could not replay diff request");
8445 }
8446 assert_eq!(
8447 logical(&state),
8448 logical(&from_diff),
8449 "diff(empty -> state) must reconstruct the tcp listener + both sni frontends"
8450 );
8451
8452 let mut torn_down = state.clone();
8454 for request in state.diff(&empty) {
8455 torn_down
8456 .dispatch(&request)
8457 .expect("could not replay teardown diff request");
8458 }
8459 assert!(
8460 torn_down.tcp_listeners.is_empty(),
8461 "diff(state -> empty) must remove the tcp listener"
8462 );
8463 assert_eq!(
8464 torn_down.count_tcp_frontends_raw(),
8465 0,
8466 "diff(state -> empty) must remove both sni frontends"
8467 );
8468
8469 let mismatched_removal = state.dispatch(
8473 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8474 cluster_id: "cluster_sni_a".to_string(),
8475 address,
8476 tags: BTreeMap::new(),
8477 sni: None,
8478 alpn: vec![],
8479 })
8480 .into(),
8481 );
8482 assert!(
8483 mismatched_removal.is_err(),
8484 "removing by address alone (no matching sni/alpn) must be rejected"
8485 );
8486 assert_eq!(
8487 state.count_tcp_frontends_raw(),
8488 2,
8489 "a rejected mismatched removal must not change either sibling"
8490 );
8491
8492 state
8495 .dispatch(&RequestType::RemoveTcpFrontend(frontend_a).into())
8496 .expect("could not remove sni-a tcp frontend");
8497 assert_eq!(state.count_tcp_frontends_raw(), 1);
8498 let remaining = state
8499 .tcp_fronts
8500 .get("cluster_sni_b")
8501 .expect("cluster_sni_b bucket must survive");
8502 assert_eq!(remaining.len(), 1);
8503 assert_eq!(remaining[0].sni, Some("other.example.com".to_string()));
8504 }
8505
8506 const TCP_PERM_CLUSTER: &str = "tcp_perm";
8507
8508 fn tcp_frontend_request(
8510 cluster_id: &str,
8511 address: SocketAddress,
8512 sni: Option<&str>,
8513 alpn: &[&str],
8514 route: &str,
8515 ) -> RequestTcpFrontend {
8516 RequestTcpFrontend {
8517 cluster_id: cluster_id.to_string(),
8518 address,
8519 tags: BTreeMap::from([("route".to_string(), route.to_string())]),
8520 sni: sni.map(str::to_string),
8521 alpn: alpn.iter().map(|protocol| protocol.to_string()).collect(),
8522 }
8523 }
8524
8525 fn tcp_permutation_fixture() -> [RequestTcpFrontend; 4] {
8528 [
8529 tcp_frontend_request(
8530 TCP_PERM_CLUSTER,
8531 SocketAddress::new_v4(127, 0, 0, 1, 7001),
8532 None,
8533 &[],
8534 "A",
8535 ),
8536 tcp_frontend_request(
8537 TCP_PERM_CLUSTER,
8538 SocketAddress::new_v4(127, 0, 0, 1, 7002),
8539 Some("b.example.com"),
8540 &["h2"],
8541 "B",
8542 ),
8543 tcp_frontend_request(
8544 TCP_PERM_CLUSTER,
8545 SocketAddress::new_v4(127, 0, 0, 1, 7002),
8546 Some("b.example.com"),
8547 &["http/1.1"],
8548 "C",
8549 ),
8550 tcp_frontend_request(
8551 TCP_PERM_CLUSTER,
8552 SocketAddress::new_v4(127, 0, 0, 1, 7003),
8553 Some("d.example.com"),
8554 &[],
8555 "D",
8556 ),
8557 ]
8558 }
8559
8560 fn tcp_state(requests: &[RequestTcpFrontend]) -> ConfigState {
8563 let mut state = ConfigState::default();
8564 for request in requests {
8565 state
8566 .dispatch(&RequestType::AddTcpFrontend(request.clone()).into())
8567 .expect("could not add the test tcp frontend");
8568 }
8569 state
8570 }
8571
8572 fn tcp_route_order(state: &ConfigState, cluster_id: &str) -> Vec<String> {
8574 state
8575 .tcp_fronts
8576 .get(cluster_id)
8577 .map(|bucket| {
8578 bucket
8579 .iter()
8580 .map(|front| front.tags["route"].clone())
8581 .collect()
8582 })
8583 .unwrap_or_default()
8584 }
8585
8586 fn tcp_semantic(state: &ConfigState) -> BTreeMap<String, BTreeSet<TcpFrontend>> {
8588 state
8589 .tcp_fronts
8590 .iter()
8591 .filter(|(_, bucket)| !bucket.is_empty())
8592 .map(|(cluster_id, bucket)| (cluster_id.to_string(), bucket.iter().cloned().collect()))
8593 .collect()
8594 }
8595
8596 fn diff_leaving_inputs_unchanged(from: &ConfigState, to: &ConfigState) -> Vec<Request> {
8598 let from_before = from.clone();
8599 let to_before = to.clone();
8600 let requests = from.diff(to);
8601 assert!(
8602 *from == from_before,
8603 "diff must leave the source state unchanged"
8604 );
8605 assert!(
8606 *to == to_before,
8607 "diff must leave the target state unchanged"
8608 );
8609 assert_eq!(
8610 from.tcp_fronts, from_before.tcp_fronts,
8611 "diff must preserve the source stored tcp order"
8612 );
8613 assert_eq!(
8614 to.tcp_fronts, to_before.tcp_fronts,
8615 "diff must preserve the target stored tcp order"
8616 );
8617 requests
8618 }
8619
8620 #[test]
8623 fn diff_tcp_pure_permutation_of_complete_frontends_emits_no_orders() {
8624 let [a, b, c, d] = tcp_permutation_fixture();
8625 let original = tcp_state(&[a.clone(), b.clone(), c.clone(), d.clone()]);
8626 let permuted = tcp_state(&[d, c, a, b]);
8627
8628 assert_eq!(
8629 tcp_route_order(&original, TCP_PERM_CLUSTER),
8630 ["A", "B", "C", "D"]
8631 );
8632 assert_eq!(
8633 tcp_route_order(&permuted, TCP_PERM_CLUSTER),
8634 ["D", "C", "A", "B"]
8635 );
8636 assert_ne!(
8637 original.tcp_fronts, permuted.tcp_fronts,
8638 "the stored vectors must differ in order for this test to be meaningful"
8639 );
8640 assert_eq!(tcp_semantic(&original), tcp_semantic(&permuted));
8641
8642 assert!(
8643 diff_leaving_inputs_unchanged(&original, &permuted).is_empty(),
8644 "a pure permutation must emit no orders"
8645 );
8646 assert!(
8647 diff_leaving_inputs_unchanged(&permuted, &original).is_empty(),
8648 "the reverse permutation must emit no orders"
8649 );
8650 }
8651
8652 #[test]
8654 fn diff_tcp_prepended_frontend_emits_only_the_semantic_addition() {
8655 let [a, _, c, _] = tcp_permutation_fixture();
8656 let from = tcp_state(std::slice::from_ref(&a));
8657 let to = tcp_state(&[c.clone(), a.clone()]);
8658 assert_eq!(tcp_route_order(&to, TCP_PERM_CLUSTER), ["C", "A"]);
8659
8660 let requests = diff_leaving_inputs_unchanged(&from, &to);
8661 let expected: Request = RequestType::AddTcpFrontend(c.clone()).into();
8662 assert_eq!(requests, vec![expected]);
8663
8664 let replayed = replay_diff(&from, &requests);
8665 assert_eq!(
8666 tcp_route_order(&replayed, TCP_PERM_CLUSTER),
8667 ["A", "C"],
8668 "replay appends the added frontend to the stored bucket"
8669 );
8670 assert_eq!(tcp_semantic(&replayed), tcp_semantic(&to));
8671
8672 let reverse = diff_leaving_inputs_unchanged(&to, &from);
8673 let expected_removal: Request = RequestType::RemoveTcpFrontend(c).into();
8674 assert_eq!(reverse, vec![expected_removal]);
8675 let reverted = replay_diff(&to, &reverse);
8676 assert_eq!(tcp_route_order(&reverted, TCP_PERM_CLUSTER), ["A"]);
8677 assert_eq!(tcp_semantic(&reverted), tcp_semantic(&from));
8678 }
8679
8680 #[test]
8684 fn diff_tcp_reconstruction_from_empty_replays_to_same_semantic_sets() {
8685 let [a, b, c, _] = tcp_permutation_fixture();
8686 let cluster_x = "tcp_x";
8687 let cluster_y = "tcp_y";
8688 let mut x_a = a;
8689 let mut x_b = b;
8690 let mut x_c = c;
8691 x_a.cluster_id = cluster_x.to_string();
8692 x_b.cluster_id = cluster_x.to_string();
8693 x_c.cluster_id = cluster_x.to_string();
8694 let y_d = tcp_frontend_request(
8695 cluster_y,
8696 SocketAddress::new_v4(127, 0, 0, 1, 7003),
8697 Some("d.example.com"),
8698 &[],
8699 "D",
8700 );
8701 let y_e = tcp_frontend_request(
8702 cluster_y,
8703 SocketAddress::new_v4(127, 0, 0, 1, 7004),
8704 None,
8705 &[],
8706 "E",
8707 );
8708
8709 let empty = ConfigState::default();
8710 let target = tcp_state(&[x_c, y_e, x_a, y_d, x_b]);
8711 assert_eq!(tcp_route_order(&target, cluster_x), ["C", "A", "B"]);
8712 assert_eq!(tcp_route_order(&target, cluster_y), ["E", "D"]);
8713
8714 let requests = diff_leaving_inputs_unchanged(&empty, &target);
8715 assert_eq!(requests.len(), 5);
8716 assert!(
8717 requests.iter().all(|request| matches!(
8718 request.request_type,
8719 Some(RequestType::AddTcpFrontend(_))
8720 )),
8721 "reconstruction from empty must only add tcp frontends"
8722 );
8723
8724 let replayed = replay_diff(&empty, &requests);
8725 assert_eq!(tcp_semantic(&replayed), tcp_semantic(&target));
8726 assert_eq!(replayed.tcp_fronts.len(), 2);
8727 assert!(
8728 replayed
8729 .tcp_fronts
8730 .values()
8731 .all(|bucket| !bucket.is_empty()),
8732 "replay must not introduce empty buckets"
8733 );
8734
8735 let teardown = diff_leaving_inputs_unchanged(&target, &empty);
8736 assert_eq!(teardown.len(), 5);
8737 let torn_down = replay_diff(&target, &teardown);
8738 assert!(tcp_semantic(&torn_down).is_empty());
8739 assert_eq!(tcp_semantic(&torn_down), tcp_semantic(&empty));
8740 }
8741
8742 #[test]
8745 fn diff_tcp_self_is_empty_and_preserves_stored_order() {
8746 let [a, b, c, d] = tcp_permutation_fixture();
8747 let state = tcp_state(&[d, a, c, b]);
8748
8749 let bucket = state
8750 .tcp_fronts
8751 .get(TCP_PERM_CLUSTER)
8752 .expect("the test cluster bucket must exist");
8753 assert!(
8754 !bucket.windows(2).all(|pair| pair[0] <= pair[1]),
8755 "the fixture bucket must be deliberately non-sorted"
8756 );
8757 assert_eq!(
8758 tcp_route_order(&state, TCP_PERM_CLUSTER),
8759 ["D", "A", "C", "B"]
8760 );
8761
8762 let before = state.clone();
8763 let stored_before = state.tcp_fronts.clone();
8764 assert!(
8765 diff_leaving_inputs_unchanged(&state, &state).is_empty(),
8766 "a state diffed against itself must emit no orders"
8767 );
8768 assert!(state == before, "self-diff must not modify the state");
8769 assert_eq!(state.tcp_fronts, stored_before);
8770 assert_eq!(
8771 tcp_route_order(&state, TCP_PERM_CLUSTER),
8772 ["D", "A", "C", "B"]
8773 );
8774 }
8775
8776 #[test]
8780 fn generate_requests_replay_preserves_non_sorted_tcp_bucket_order() {
8781 let [a, b, c, d] = tcp_permutation_fixture();
8782 let state = tcp_state(&[d, a, c, b]);
8783
8784 let bucket = state
8785 .tcp_fronts
8786 .get(TCP_PERM_CLUSTER)
8787 .expect("the test cluster bucket must exist");
8788 assert!(
8789 !bucket.windows(2).all(|pair| pair[0] <= pair[1]),
8790 "the fixture bucket must be deliberately non-sorted"
8791 );
8792
8793 let mut replayed = ConfigState::default();
8794 for request in state.generate_requests() {
8795 replayed
8796 .dispatch(&request)
8797 .expect("could not replay generated request");
8798 }
8799
8800 let logical = |s: &ConfigState| {
8803 let mut c = s.clone();
8804 c.request_counts.clear();
8805 c
8806 };
8807 assert!(
8808 logical(&state) == logical(&replayed),
8809 "generate_requests -> replay must reproduce the state"
8810 );
8811 assert_eq!(
8812 replayed.tcp_fronts.get(TCP_PERM_CLUSTER),
8813 state.tcp_fronts.get(TCP_PERM_CLUSTER),
8814 "the replayed bucket must equal the original Vec, order included"
8815 );
8816 assert_eq!(
8817 tcp_route_order(&replayed, TCP_PERM_CLUSTER),
8818 ["D", "A", "C", "B"]
8819 );
8820 }
8821
8822 fn add_tcp_test_cluster(state: &mut ConfigState, cluster_id: &str) {
8833 state
8834 .dispatch(
8835 &RequestType::AddCluster(Cluster {
8836 cluster_id: cluster_id.to_string(),
8837 sticky_session: false,
8838 https_redirect: false,
8839 ..Default::default()
8840 })
8841 .into(),
8842 )
8843 .expect("could not add cluster");
8844 }
8845
8846 #[test]
8851 fn add_tcp_frontend_rejects_cross_cluster_duplicate_identity() {
8852 let address = SocketAddress::new_v4(127, 0, 0, 1, 9001);
8853 let mut state = ConfigState::new();
8854 add_tcp_test_cluster(&mut state, "cluster_x");
8855 add_tcp_test_cluster(&mut state, "cluster_y");
8856
8857 state
8858 .dispatch(
8859 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8860 cluster_id: "cluster_x".to_string(),
8861 address,
8862 tags: BTreeMap::new(),
8863 sni: Some("example.com".to_string()),
8864 alpn: vec!["h2".to_string()],
8865 })
8866 .into(),
8867 )
8868 .expect("could not add cluster_x tcp frontend");
8869
8870 let hash_before = state.hash_state();
8871 let count_before = state.count_tcp_frontends_raw();
8872
8873 let result = state.dispatch(
8874 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8875 cluster_id: "cluster_y".to_string(),
8876 address,
8877 tags: BTreeMap::new(),
8878 sni: Some("example.com".to_string()),
8879 alpn: vec!["h2".to_string()],
8880 })
8881 .into(),
8882 );
8883 assert!(
8884 result.is_err(),
8885 "the same (address, sni, alpn) under a different cluster_id must be rejected, \
8886 got: {result:?}"
8887 );
8888 assert_eq!(
8889 state.hash_state(),
8890 hash_before,
8891 "a rejected cross-cluster duplicate must not change the state hash"
8892 );
8893 assert_eq!(
8894 state.count_tcp_frontends_raw(),
8895 count_before,
8896 "a rejected cross-cluster duplicate must not change the frontend count"
8897 );
8898 assert_eq!(state.tcp_fronts.get("cluster_x").map(Vec::len), Some(1));
8899 assert_eq!(
8900 state.tcp_fronts.get("cluster_y").map(Vec::len).unwrap_or(0),
8901 0
8902 );
8903 }
8904
8905 #[test]
8908 fn add_tcp_frontend_rejects_reordered_alpn_duplicate() {
8909 let address = SocketAddress::new_v4(127, 0, 0, 1, 9002);
8910 let mut state = ConfigState::new();
8911 add_tcp_test_cluster(&mut state, "cluster_reorder");
8912
8913 state
8914 .dispatch(
8915 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8916 cluster_id: "cluster_reorder".to_string(),
8917 address,
8918 tags: BTreeMap::new(),
8919 sni: Some("example.com".to_string()),
8920 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8921 })
8922 .into(),
8923 )
8924 .expect("could not add first tcp frontend");
8925
8926 let hash_before = state.hash_state();
8927 let count_before = state.count_tcp_frontends_raw();
8928
8929 let result = state.dispatch(
8930 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8931 cluster_id: "cluster_reorder".to_string(),
8932 address,
8933 tags: BTreeMap::new(),
8934 sni: Some("example.com".to_string()),
8935 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8936 })
8937 .into(),
8938 );
8939 assert!(
8940 result.is_err(),
8941 "the same ALPN set in a different order must be rejected as a duplicate \
8942 identity, got: {result:?}"
8943 );
8944 assert_eq!(state.hash_state(), hash_before);
8945 assert_eq!(state.count_tcp_frontends_raw(), count_before);
8946 assert_eq!(
8947 state.tcp_fronts.get("cluster_reorder").map(Vec::len),
8948 Some(1)
8949 );
8950 }
8951
8952 #[test]
8956 fn remove_tcp_frontend_matches_reordered_alpn() {
8957 let address = SocketAddress::new_v4(127, 0, 0, 1, 9003);
8958 let mut state = ConfigState::new();
8959 add_tcp_test_cluster(&mut state, "cluster_remove_reorder");
8960
8961 state
8962 .dispatch(
8963 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8964 cluster_id: "cluster_remove_reorder".to_string(),
8965 address,
8966 tags: BTreeMap::new(),
8967 sni: Some("example.com".to_string()),
8968 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8969 })
8970 .into(),
8971 )
8972 .expect("could not add tcp frontend");
8973
8974 let result = state.dispatch(
8975 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8976 cluster_id: "cluster_remove_reorder".to_string(),
8977 address,
8978 tags: BTreeMap::new(),
8979 sni: Some("example.com".to_string()),
8980 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8981 })
8982 .into(),
8983 );
8984 assert!(
8985 result.is_ok(),
8986 "removing with the same ALPN set in a different order must succeed, got: {result:?}"
8987 );
8988 assert_eq!(state.count_tcp_frontends_raw(), 0);
8989 }
8990
8991 #[test]
8995 fn add_tcp_frontend_rejects_alpn_overlap_across_clusters_same_sni() {
8996 let address = SocketAddress::new_v4(127, 0, 0, 1, 9004);
8997 let mut state = ConfigState::new();
8998 add_tcp_test_cluster(&mut state, "cluster_overlap_a");
8999 add_tcp_test_cluster(&mut state, "cluster_overlap_b");
9000
9001 state
9002 .dispatch(
9003 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9004 cluster_id: "cluster_overlap_a".to_string(),
9005 address,
9006 tags: BTreeMap::new(),
9007 sni: Some("example.com".to_string()),
9008 alpn: vec!["h2".to_string()],
9009 })
9010 .into(),
9011 )
9012 .expect("could not add cluster_overlap_a tcp frontend");
9013
9014 let hash_before = state.hash_state();
9015 let count_before = state.count_tcp_frontends_raw();
9016
9017 let result = state.dispatch(
9018 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9019 cluster_id: "cluster_overlap_b".to_string(),
9020 address,
9021 tags: BTreeMap::new(),
9022 sni: Some("example.com".to_string()),
9023 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
9024 })
9025 .into(),
9026 );
9027 assert!(
9028 result.is_err(),
9029 "an overlapping ALPN set on the same (address, sni) under a different cluster_id \
9030 must be rejected, got: {result:?}"
9031 );
9032 assert_eq!(state.hash_state(), hash_before);
9033 assert_eq!(state.count_tcp_frontends_raw(), count_before);
9034 assert_eq!(
9035 state
9036 .tcp_fronts
9037 .get("cluster_overlap_b")
9038 .map(Vec::len)
9039 .unwrap_or(0),
9040 0
9041 );
9042 }
9043
9044 #[test]
9047 fn add_tcp_frontend_rejects_no_sni_sni_mixing_both_directions() {
9048 let address_a = SocketAddress::new_v4(127, 0, 0, 1, 9005);
9049 let address_b = SocketAddress::new_v4(127, 0, 0, 1, 9006);
9050 let mut state = ConfigState::new();
9051 add_tcp_test_cluster(&mut state, "cluster_mix_1");
9052 add_tcp_test_cluster(&mut state, "cluster_mix_2");
9053 add_tcp_test_cluster(&mut state, "cluster_mix_3");
9054 add_tcp_test_cluster(&mut state, "cluster_mix_4");
9055
9056 state
9059 .dispatch(
9060 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9061 cluster_id: "cluster_mix_1".to_string(),
9062 address: address_a,
9063 tags: BTreeMap::new(),
9064 sni: None,
9065 alpn: vec![],
9066 })
9067 .into(),
9068 )
9069 .expect("could not add no-sni tcp frontend");
9070 let hash_before_a = state.hash_state();
9071 let count_before_a = state.count_tcp_frontends_raw();
9072 let result_a = state.dispatch(
9073 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9074 cluster_id: "cluster_mix_2".to_string(),
9075 address: address_a,
9076 tags: BTreeMap::new(),
9077 sni: Some("example.com".to_string()),
9078 alpn: vec![],
9079 })
9080 .into(),
9081 );
9082 assert!(
9083 result_a.is_err(),
9084 "an SNI-scoped frontend must not be added to a listener with an existing no-SNI \
9085 front, got: {result_a:?}"
9086 );
9087 assert_eq!(state.hash_state(), hash_before_a);
9088 assert_eq!(state.count_tcp_frontends_raw(), count_before_a);
9089
9090 state
9093 .dispatch(
9094 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9095 cluster_id: "cluster_mix_3".to_string(),
9096 address: address_b,
9097 tags: BTreeMap::new(),
9098 sni: Some("example.com".to_string()),
9099 alpn: vec![],
9100 })
9101 .into(),
9102 )
9103 .expect("could not add sni-scoped tcp frontend");
9104 let hash_before_b = state.hash_state();
9105 let count_before_b = state.count_tcp_frontends_raw();
9106 let result_b = state.dispatch(
9107 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9108 cluster_id: "cluster_mix_4".to_string(),
9109 address: address_b,
9110 tags: BTreeMap::new(),
9111 sni: None,
9112 alpn: vec![],
9113 })
9114 .into(),
9115 );
9116 assert!(
9117 result_b.is_err(),
9118 "a no-SNI frontend must not be added to a listener with existing SNI-scoped \
9119 fronts, got: {result_b:?}"
9120 );
9121 assert_eq!(state.hash_state(), hash_before_b);
9122 assert_eq!(state.count_tcp_frontends_raw(), count_before_b);
9123 }
9124
9125 #[test]
9130 fn add_tcp_frontend_rejects_malformed_sni_shape() {
9131 let address = SocketAddress::new_v4(127, 0, 0, 1, 9007);
9132 let mut state = ConfigState::new();
9133 add_tcp_test_cluster(&mut state, "cluster_malformed");
9134
9135 let hash_before = state.hash_state();
9136 let count_before = state.count_tcp_frontends_raw();
9137
9138 let result = state.dispatch(
9139 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9140 cluster_id: "cluster_malformed".to_string(),
9141 address,
9142 tags: BTreeMap::new(),
9143 sni: Some(".example.com".to_string()),
9145 alpn: vec![],
9146 })
9147 .into(),
9148 );
9149 assert!(
9150 result.is_err(),
9151 "a malformed SNI pattern must be rejected by the master, not merely by \
9152 config-load, got: {result:?}"
9153 );
9154 assert_eq!(state.hash_state(), hash_before);
9155 assert_eq!(state.count_tcp_frontends_raw(), count_before);
9156 assert_eq!(
9157 state
9158 .tcp_fronts
9159 .get("cluster_malformed")
9160 .map(Vec::len)
9161 .unwrap_or(0),
9162 0
9163 );
9164 }
9165
9166 #[test]
9172 fn add_tcp_frontend_rejects_alpn_without_sni() {
9173 let address = SocketAddress::new_v4(127, 0, 0, 1, 9009);
9174 let mut state = ConfigState::new();
9175 add_tcp_test_cluster(&mut state, "cluster_alpn_no_sni");
9176
9177 let hash_before = state.hash_state();
9178 let count_before = state.count_tcp_frontends_raw();
9179
9180 let result = state.dispatch(
9181 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9182 cluster_id: "cluster_alpn_no_sni".to_string(),
9183 address,
9184 tags: BTreeMap::new(),
9185 sni: None,
9186 alpn: vec!["h2".to_string()],
9187 })
9188 .into(),
9189 );
9190 assert!(
9191 result.is_err(),
9192 "a non-empty alpn without sni must be rejected by the master, got: {result:?}"
9193 );
9194 assert_eq!(
9195 state.hash_state(),
9196 hash_before,
9197 "a rejected alpn-without-sni add must not change the state hash"
9198 );
9199 assert_eq!(
9200 state.count_tcp_frontends_raw(),
9201 count_before,
9202 "a rejected alpn-without-sni add must not change the frontend count"
9203 );
9204 assert_eq!(
9205 state
9206 .tcp_fronts
9207 .get("cluster_alpn_no_sni")
9208 .map(Vec::len)
9209 .unwrap_or(0),
9210 0
9211 );
9212 }
9213
9214 #[test]
9220 fn add_tcp_frontend_accepts_exact_and_wildcard_catch_all_siblings() {
9221 let address = SocketAddress::new_v4(127, 0, 0, 1, 9008);
9222 let mut state = ConfigState::new();
9223 add_tcp_test_cluster(&mut state, "cluster_exact_catchall");
9224 add_tcp_test_cluster(&mut state, "cluster_wildcard_catchall");
9225
9226 state
9227 .dispatch(
9228 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9229 cluster_id: "cluster_exact_catchall".to_string(),
9230 address,
9231 tags: BTreeMap::new(),
9232 sni: Some("example.com".to_string()),
9233 alpn: vec![],
9234 })
9235 .into(),
9236 )
9237 .expect("could not add exact catch-all tcp frontend");
9238
9239 let result = state.dispatch(
9240 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9241 cluster_id: "cluster_wildcard_catchall".to_string(),
9242 address,
9243 tags: BTreeMap::new(),
9244 sni: Some("*.example.com".to_string()),
9245 alpn: vec![],
9246 })
9247 .into(),
9248 );
9249 assert!(
9250 result.is_ok(),
9251 "an exact catch-all and a wildcard catch-all on the same address must both be \
9252 accepted, got: {result:?}"
9253 );
9254 assert_eq!(state.count_tcp_frontends_raw(), 2);
9255 }
9256
9257 #[test]
9261 fn list_frontends_includes_udp() {
9262 let tcp_addr = SocketAddress::new_v4(0, 0, 0, 0, 6379);
9263 let udp_addr = SocketAddress::new_v4(0, 0, 0, 0, 5353);
9264
9265 let mut state = ConfigState::default();
9266 state
9267 .dispatch(
9268 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9269 cluster_id: "tcp_cluster".to_string(),
9270 address: tcp_addr,
9271 ..Default::default()
9272 })
9273 .into(),
9274 )
9275 .expect("could not add tcp frontend");
9276 state
9277 .dispatch(
9278 &RequestType::AddUdpFrontend(RequestUdpFrontend {
9279 cluster_id: "udp_cluster".to_string(),
9280 address: udp_addr,
9281 ..Default::default()
9282 })
9283 .into(),
9284 )
9285 .expect("could not add udp frontend");
9286
9287 let all = state.list_frontends(FrontendFilters::default());
9289 assert_eq!(all.tcp_frontends.len(), 1, "tcp frontend must be listed");
9290 assert_eq!(
9291 all.udp_frontends.len(),
9292 1,
9293 "udp frontend must be listed under the default all-pass path"
9294 );
9295 assert_eq!(all.udp_frontends[0].cluster_id, "udp_cluster");
9296 assert_eq!(all.udp_frontends[0].address, udp_addr);
9297
9298 let tcp_only = state.list_frontends(FrontendFilters {
9300 tcp: true,
9301 ..Default::default()
9302 });
9303 assert_eq!(tcp_only.tcp_frontends.len(), 1);
9304 assert_eq!(
9305 tcp_only.udp_frontends.len(),
9306 1,
9307 "udp frontends ride the tcp filter"
9308 );
9309
9310 let http_only = state.list_frontends(FrontendFilters {
9312 http: true,
9313 ..Default::default()
9314 });
9315 assert!(http_only.tcp_frontends.is_empty());
9316 assert!(http_only.udp_frontends.is_empty());
9317
9318 let domain_filtered = state.list_frontends(FrontendFilters {
9320 domain: Some("example.com".to_string()),
9321 ..Default::default()
9322 });
9323 assert!(domain_filtered.tcp_frontends.is_empty());
9324 assert!(domain_filtered.udp_frontends.is_empty());
9325 }
9326
9327 #[test]
9331 fn list_frontends_filters_by_cluster_id() {
9332 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
9333 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9334 let mut state = ConfigState::default();
9335 for (cluster_id, hostname) in [
9336 (Some(String::from("wanted")), "wanted.example.com"),
9337 (Some(String::from("other")), "other.example.com"),
9338 (None, "denied.example.com"),
9340 ] {
9341 state
9342 .dispatch(
9343 &RequestType::AddHttpFrontend(RequestHttpFrontend {
9344 cluster_id: cluster_id.clone(),
9345 hostname: hostname.to_string(),
9346 path: PathRule::prefix(String::from("/")),
9347 address: http_addr,
9348 position: RulePosition::Tree.into(),
9349 ..Default::default()
9350 })
9351 .into(),
9352 )
9353 .expect("could not add http frontend");
9354 state
9355 .dispatch(
9356 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
9357 cluster_id,
9358 hostname: hostname.to_string(),
9359 path: PathRule::prefix(String::from("/")),
9360 address: https_addr,
9361 position: RulePosition::Tree.into(),
9362 ..Default::default()
9363 })
9364 .into(),
9365 )
9366 .expect("could not add https frontend");
9367 }
9368 for (cluster_id, port) in [("wanted", 6379u16), ("other", 6380)] {
9370 state
9371 .dispatch(
9372 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9373 cluster_id: cluster_id.to_string(),
9374 address: SocketAddress::new_v4(0, 0, 0, 0, port),
9375 ..Default::default()
9376 })
9377 .into(),
9378 )
9379 .expect("could not add tcp frontend");
9380 state
9381 .dispatch(
9382 &RequestType::AddUdpFrontend(RequestUdpFrontend {
9383 cluster_id: cluster_id.to_string(),
9384 address: SocketAddress::new_v4(0, 0, 0, 0, port + 1000),
9385 ..Default::default()
9386 })
9387 .into(),
9388 )
9389 .expect("could not add udp frontend");
9390 }
9391
9392 let all = state.list_frontends(FrontendFilters::default());
9394 assert_eq!(all.http_frontends.len(), 3);
9395 assert_eq!(all.https_frontends.len(), 3);
9396 assert_eq!(all.tcp_frontends.len(), 2);
9397 assert_eq!(all.udp_frontends.len(), 2);
9398
9399 let wanted = state.list_frontends(FrontendFilters {
9402 cluster_id: Some("wanted".to_string()),
9403 ..Default::default()
9404 });
9405 assert_eq!(wanted.http_frontends.len(), 1);
9406 assert_eq!(wanted.https_frontends.len(), 1);
9407 assert_eq!(wanted.tcp_frontends.len(), 1);
9408 assert_eq!(wanted.udp_frontends.len(), 1);
9409 assert_eq!(
9410 wanted.http_frontends[0].cluster_id.as_deref(),
9411 Some("wanted")
9412 );
9413 assert_eq!(wanted.tcp_frontends[0].cluster_id, "wanted");
9414 assert_eq!(wanted.udp_frontends[0].cluster_id, "wanted");
9415 assert!(
9416 wanted
9417 .http_frontends
9418 .iter()
9419 .chain(wanted.https_frontends.iter())
9420 .all(|f| f.hostname == "wanted.example.com"),
9421 "no frontend of another cluster may leak through the filter"
9422 );
9423
9424 let unknown = state.list_frontends(FrontendFilters {
9426 cluster_id: Some("nope".to_string()),
9427 ..Default::default()
9428 });
9429 assert!(unknown.http_frontends.is_empty());
9430 assert!(unknown.https_frontends.is_empty());
9431 assert!(unknown.tcp_frontends.is_empty());
9432 assert!(unknown.udp_frontends.is_empty());
9433
9434 let conflicting = state.list_frontends(FrontendFilters {
9436 cluster_id: Some("wanted".to_string()),
9437 domain: Some("other.example.com".to_string()),
9438 ..Default::default()
9439 });
9440 assert!(conflicting.http_frontends.is_empty());
9441 assert!(conflicting.https_frontends.is_empty());
9442
9443 let https_only = state.list_frontends(FrontendFilters {
9445 https: true,
9446 cluster_id: Some("wanted".to_string()),
9447 ..Default::default()
9448 });
9449 assert!(https_only.http_frontends.is_empty());
9450 assert_eq!(https_only.https_frontends.len(), 1);
9451 assert!(https_only.tcp_frontends.is_empty());
9452 assert!(https_only.udp_frontends.is_empty());
9453 }
9454
9455 #[test]
9460 fn update_https_listener_happy_path_h2_knobs() {
9461 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9462 let mut state = ConfigState::new();
9463 state
9464 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9465 .unwrap();
9466
9467 let patch = UpdateHttpsListenerConfig {
9468 address: addr,
9469 h2_max_rst_stream_per_window: Some(50),
9470 h2_max_ping_per_window: Some(20),
9471 ..Default::default()
9472 };
9473 state
9474 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9475 .expect("update must succeed");
9476
9477 let listener = state
9478 .https_listeners
9479 .get(&ListenerKey::from(addr))
9480 .expect("listener must be present");
9481 assert_eq!(listener.h2_max_rst_stream_per_window, Some(50));
9482 assert_eq!(listener.h2_max_ping_per_window, Some(20));
9483 assert_eq!(listener.front_timeout, 60);
9485 assert_eq!(listener.h2_max_settings_per_window, None);
9486 }
9487
9488 #[test]
9491 fn update_https_listener_not_found() {
9492 let mut state = ConfigState::new();
9493 let patch = UpdateHttpsListenerConfig {
9494 address: SocketAddress::new_v4(1, 2, 3, 4, 9999),
9495 h2_max_rst_stream_per_window: Some(50),
9496 ..Default::default()
9497 };
9498 let err = state
9499 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9500 .unwrap_err();
9501 assert!(
9502 matches!(
9503 err,
9504 StateError::NotFound {
9505 kind: ObjectKind::HttpsListener,
9506 ..
9507 }
9508 ),
9509 "expected NotFound, got: {err}"
9510 );
9511 }
9512
9513 #[test]
9516 fn update_https_listener_noop_patch() {
9517 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9518 let mut state = ConfigState::new();
9519 let original = make_https_listener(addr);
9520 state
9521 .dispatch(&RequestType::AddHttpsListener(original.clone()).into())
9522 .unwrap();
9523
9524 let patch = UpdateHttpsListenerConfig {
9525 address: addr,
9526 ..Default::default()
9527 };
9528 state
9529 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9530 .expect("no-op patch must succeed");
9531
9532 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
9533 assert_eq!(listener.front_timeout, original.front_timeout);
9534 assert_eq!(
9535 listener.h2_max_rst_stream_per_window,
9536 original.h2_max_rst_stream_per_window
9537 );
9538 }
9539
9540 #[test]
9542 fn update_https_listener_invalid_value_flood_knob_zero() {
9543 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9544 let mut state = ConfigState::new();
9545 state
9546 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9547 .unwrap();
9548
9549 let patch = UpdateHttpsListenerConfig {
9550 address: addr,
9551 h2_max_rst_stream_per_window: Some(0),
9552 ..Default::default()
9553 };
9554 let err = state
9555 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9556 .unwrap_err();
9557 assert!(
9558 matches!(
9559 err,
9560 StateError::InvalidValue {
9561 field: "h2_max_rst_stream_per_window",
9562 ..
9563 }
9564 ),
9565 "expected InvalidValue for flood knob 0, got: {err}"
9566 );
9567 }
9568
9569 #[test]
9575 fn a_tcp_frontend_and_an_affinity_key_never_share_a_cluster() {
9576 use crate::proto::command::RequestTcpFrontend;
9577
9578 let keyed = |cluster_id: &str| -> Request {
9579 RequestType::AddCluster(Cluster {
9580 cluster_id: cluster_id.to_owned(),
9581 affinity_header: Some(String::from("X-Tenant")),
9582 ..Default::default()
9583 })
9584 .into()
9585 };
9586 let tcp_front = |cluster_id: &str, port: u16| -> Request {
9587 RequestType::AddTcpFrontend(RequestTcpFrontend {
9588 cluster_id: cluster_id.to_owned(),
9589 address: SocketAddress::new_v4(127, 0, 0, 1, port),
9590 ..Default::default()
9591 })
9592 .into()
9593 };
9594
9595 let mut state = ConfigState::new();
9597 state
9598 .dispatch(&keyed("keyed"))
9599 .expect("a keyed cluster is valid");
9600 let err = state
9601 .dispatch(&tcp_front("keyed", 4000))
9602 .expect_err("a TCP frontend may not join a keyed cluster");
9603 assert!(
9604 matches!(err, StateError::InvalidTcpFrontend { .. }),
9605 "unexpected error: {err:?}"
9606 );
9607 assert!(state.tcp_fronts.get("keyed").is_none_or(Vec::is_empty));
9608
9609 let mut state = ConfigState::new();
9611 state
9612 .dispatch(
9613 &RequestType::AddCluster(Cluster {
9614 cluster_id: String::from("tcp"),
9615 ..Default::default()
9616 })
9617 .into(),
9618 )
9619 .expect("a plain cluster is valid");
9620 state
9621 .dispatch(&tcp_front("tcp", 4001))
9622 .expect("a TCP frontend may join a plain cluster");
9623 let err = state
9624 .dispatch(&keyed("tcp"))
9625 .expect_err("a cluster serving TCP may not name a key");
9626 assert!(
9627 matches!(
9628 err,
9629 StateError::InvalidValue {
9630 field: "affinity_key",
9631 ..
9632 }
9633 ),
9634 "unexpected error: {err:?}"
9635 );
9636 assert_eq!(state.clusters["tcp"].affinity_header, None);
9637 }
9638
9639 #[test]
9643 fn add_cluster_with_out_of_range_max_connection_attempts_rejected() {
9644 for attempts in [0, 256] {
9645 let mut state = ConfigState::new();
9646 let err = state
9647 .dispatch(
9648 &RequestType::AddCluster(Cluster {
9649 cluster_id: String::from("budget"),
9650 max_connection_attempts: Some(attempts),
9651 ..Default::default()
9652 })
9653 .into(),
9654 )
9655 .expect_err("an out-of-range budget must be refused");
9656 assert!(
9657 matches!(
9658 err,
9659 StateError::InvalidValue {
9660 field: "max_connection_attempts",
9661 ..
9662 }
9663 ),
9664 "{attempts}: unexpected error {err:?}"
9665 );
9666 assert!(state.clusters.is_empty());
9667 }
9668 let mut state = ConfigState::new();
9669 state
9670 .dispatch(
9671 &RequestType::AddCluster(Cluster {
9672 cluster_id: String::from("budget"),
9673 max_connection_attempts: Some(7),
9674 ..Default::default()
9675 })
9676 .into(),
9677 )
9678 .expect("a budget in range is accepted");
9679 assert_eq!(state.clusters["budget"].max_connection_attempts, Some(7));
9680 }
9681
9682 #[test]
9683 fn add_cluster_with_invalid_shuffle_sharding_rejected() {
9684 for (percent, minimum, mode) in [
9685 (Some(0), None, None),
9686 (Some(101), None, None),
9687 (Some(50), Some(1), None),
9688 (None, Some(8), None),
9689 (None, None, Some(1)),
9690 (Some(50), None, Some(7)),
9691 ] {
9692 let mut state = ConfigState::new();
9693 let err = state
9694 .dispatch(
9695 &RequestType::AddCluster(Cluster {
9696 cluster_id: String::from("sharded"),
9697 shard_percent: percent,
9698 shard_min_backends: minimum,
9699 shard_mode: mode,
9700 ..Default::default()
9701 })
9702 .into(),
9703 )
9704 .expect_err("invalid shuffle sharding must be refused");
9705 assert!(
9706 matches!(
9707 err,
9708 StateError::InvalidValue {
9709 field: "shuffle_sharding",
9710 ..
9711 }
9712 ),
9713 "({percent:?}, {minimum:?}, {mode:?}): unexpected error {err:?}"
9714 );
9715 assert!(state.clusters.is_empty());
9716 }
9717 let mut state = ConfigState::new();
9718 state
9719 .dispatch(
9720 &RequestType::AddCluster(Cluster {
9721 cluster_id: String::from("sharded"),
9722 shard_percent: Some(25),
9723 shard_min_backends: Some(4),
9724 shard_mode: Some(1),
9725 ..Default::default()
9726 })
9727 .into(),
9728 )
9729 .expect("a valid sharded cluster is accepted");
9730 }
9731
9732 #[test]
9733 fn add_cluster_with_two_affinity_key_sources_rejected() {
9734 let mut state = ConfigState::new();
9735 let err = state
9736 .dispatch(
9737 &RequestType::AddCluster(Cluster {
9738 cluster_id: String::from("keyed"),
9739 affinity_header: Some(String::from("X-Tenant")),
9740 affinity_cookie: Some(String::from("tenant")),
9741 ..Default::default()
9742 })
9743 .into(),
9744 )
9745 .expect_err("a cluster keyed on both a header and a cookie must be refused");
9746 assert!(
9747 matches!(
9748 err,
9749 StateError::InvalidValue {
9750 field: "affinity_key",
9751 ..
9752 }
9753 ),
9754 "unexpected error: {err:?}"
9755 );
9756 assert!(
9757 state.clusters.is_empty(),
9758 "a refused cluster must not be stored"
9759 );
9760 }
9761
9762 #[test]
9763 fn add_cluster_invalid_health_check_uri_rejected() {
9764 use crate::proto::command::HealthCheckConfig;
9765
9766 let mut state = ConfigState::new();
9767 let err = state
9768 .dispatch(
9769 &RequestType::AddCluster(Cluster {
9770 cluster_id: String::from("evil_cluster"),
9771 health_check: Some(HealthCheckConfig {
9772 uri: String::from("/foo\r\nGET /admin"),
9773 interval: 5_000,
9774 timeout: 1_000,
9775 healthy_threshold: 2,
9776 unhealthy_threshold: 2,
9777 ..Default::default()
9778 }),
9779 ..Default::default()
9780 })
9781 .into(),
9782 )
9783 .unwrap_err();
9784
9785 assert!(
9786 matches!(
9787 err,
9788 StateError::InvalidValue {
9789 field: "health_check",
9790 ..
9791 }
9792 ),
9793 "expected InvalidValue for CRLF-bearing health-check URI, got: {err}"
9794 );
9795 assert!(
9796 !state.clusters.contains_key("evil_cluster"),
9797 "cluster must not be inserted when health_check fails validation",
9798 );
9799 }
9800
9801 #[test]
9805 fn set_health_check_tcp_mode_with_status_field_rejected() {
9806 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9807
9808 let mut state = ConfigState::new();
9809 state
9810 .dispatch(
9811 &RequestType::AddCluster(Cluster {
9812 cluster_id: String::from("app"),
9813 ..Default::default()
9814 })
9815 .into(),
9816 )
9817 .expect("the cluster is added");
9818 let tcp = HealthCheckConfig {
9819 uri: String::new(),
9820 interval: 10,
9821 timeout: 5,
9822 healthy_threshold: 3,
9823 unhealthy_threshold: 3,
9824 mode: HealthCheckMode::Tcp as i32,
9825 ..Default::default()
9826 };
9827 let set = |config: HealthCheckConfig| {
9828 RequestType::SetHealthCheck(SetHealthCheck {
9829 cluster_id: String::from("app"),
9830 config,
9831 })
9832 .into()
9833 };
9834
9835 let err = state
9836 .dispatch(&set(HealthCheckConfig {
9837 expected_status: 200,
9838 ..tcp.clone()
9839 }))
9840 .unwrap_err();
9841 assert!(
9842 matches!(
9843 err,
9844 StateError::InvalidValue {
9845 field: "health_check",
9846 ..
9847 }
9848 ),
9849 "expected InvalidValue for a TCP probe with expected_status, got: {err}"
9850 );
9851 assert_eq!(state.clusters["app"].health_check, None);
9852
9853 state
9854 .dispatch(&set(tcp.clone()))
9855 .expect("a bare TCP probe is accepted");
9856 assert_eq!(state.clusters["app"].health_check, Some(tcp));
9857 }
9858
9859 #[test]
9862 fn set_health_check_out_of_range_port_rejected() {
9863 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9864
9865 let mut state = ConfigState::new();
9866 state
9867 .dispatch(
9868 &RequestType::AddCluster(Cluster {
9869 cluster_id: String::from("app"),
9870 ..Default::default()
9871 })
9872 .into(),
9873 )
9874 .expect("the cluster is added");
9875 let probe = HealthCheckConfig {
9876 uri: String::new(),
9877 interval: 10,
9878 timeout: 5,
9879 healthy_threshold: 3,
9880 unhealthy_threshold: 3,
9881 mode: HealthCheckMode::Tcp as i32,
9882 port: Some(8081),
9883 ..Default::default()
9884 };
9885 let set = |config: HealthCheckConfig| {
9886 RequestType::SetHealthCheck(SetHealthCheck {
9887 cluster_id: String::from("app"),
9888 config,
9889 })
9890 .into()
9891 };
9892
9893 for port in [0, 65536] {
9894 let err = state
9895 .dispatch(&set(HealthCheckConfig {
9896 port: Some(port),
9897 ..probe.clone()
9898 }))
9899 .unwrap_err();
9900 assert!(
9901 matches!(
9902 err,
9903 StateError::InvalidValue {
9904 field: "health_check",
9905 ..
9906 }
9907 ),
9908 "expected InvalidValue for probe port {port}, got: {err}"
9909 );
9910 assert_eq!(state.clusters["app"].health_check, None);
9911 }
9912
9913 state
9914 .dispatch(&set(probe.clone()))
9915 .expect("a probe port within 1..=65535 is accepted");
9916 assert_eq!(state.clusters["app"].health_check, Some(probe));
9917 }
9918
9919 #[test]
9923 fn health_check_headers_validate_before_mutation_and_survive_replay() {
9924 use std::io::Read;
9925
9926 use crate::{
9927 parser::parse_several_requests,
9928 proto::command::{
9929 HealthCheckConfig, HealthCheckHeader, HealthCheckMode, SetHealthCheck,
9930 },
9931 };
9932
9933 let header = |name: &str, tag: &str| HealthCheckHeader {
9935 name: name.to_owned(),
9936 fingerprint: format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 7, tag.len()),
9937 };
9938 let probe = |headers: Vec<HealthCheckHeader>| HealthCheckConfig {
9939 uri: "/health".to_owned(),
9940 interval: 10,
9941 timeout: 5,
9942 healthy_threshold: 3,
9943 unhealthy_threshold: 3,
9944 mode: HealthCheckMode::Http as i32,
9945 headers,
9946 ..Default::default()
9947 };
9948 let ordered = vec![
9949 header("Z-Last", "1"),
9950 header("Authorization", "Bearer"),
9951 header("A-First", ""),
9952 ];
9953 let set = |config: HealthCheckConfig| -> Request {
9954 RequestType::SetHealthCheck(SetHealthCheck {
9955 cluster_id: String::from("app"),
9956 config,
9957 })
9958 .into()
9959 };
9960 let invalid = probe(vec![header("Host", "1")]);
9962
9963 let mut state = ConfigState::new();
9965 let err = state
9966 .dispatch(
9967 &RequestType::AddCluster(Cluster {
9968 cluster_id: String::from("app"),
9969 health_check: Some(invalid.clone()),
9970 ..Default::default()
9971 })
9972 .into(),
9973 )
9974 .unwrap_err();
9975 assert!(
9976 matches!(
9977 err,
9978 StateError::InvalidValue {
9979 field: "health_check",
9980 ..
9981 }
9982 ),
9983 "got {err}"
9984 );
9985 assert!(state.clusters.is_empty(), "no cluster may be inserted");
9986
9987 state
9988 .dispatch(
9989 &RequestType::AddCluster(Cluster {
9990 cluster_id: String::from("app"),
9991 ..Default::default()
9992 })
9993 .into(),
9994 )
9995 .expect("the cluster is added");
9996 state
9997 .dispatch(&set(probe(ordered.clone())))
9998 .expect("a valid header list is accepted");
9999
10000 let before = state.clone();
10002 let err = state.dispatch(&set(invalid)).unwrap_err();
10003 assert!(
10004 matches!(
10005 err,
10006 StateError::InvalidValue {
10007 field: "health_check",
10008 ..
10009 }
10010 ),
10011 "got {err}"
10012 );
10013 let logical = |s: &ConfigState| {
10015 let mut c = s.clone();
10016 c.request_counts.clear();
10017 c
10018 };
10019 assert_eq!(
10020 logical(&state),
10021 logical(&before),
10022 "a refused order must not mutate the state"
10023 );
10024 assert_eq!(
10025 state.clusters["app"].health_check.as_ref().unwrap().headers,
10026 ordered
10027 );
10028
10029 let queried = state.cluster_state("app").expect("cluster exists");
10031 assert_eq!(
10032 queried.configuration.unwrap().health_check.unwrap().headers,
10033 ordered
10034 );
10035 assert_eq!(
10036 state.list_health_checks(Some("app")).map["app"].headers,
10037 ordered
10038 );
10039
10040 let adds = ConfigState::new().diff(&state);
10042 let carried = adds.iter().any(|request| {
10043 matches!(
10044 &request.request_type,
10045 Some(RequestType::AddCluster(cluster))
10046 if cluster.health_check.as_ref().is_some_and(|hc| hc.headers == ordered)
10047 )
10048 });
10049 assert!(carried, "the diff must carry the ordered headers: {adds:?}");
10050 let mut reordered = state.clone();
10051 reordered
10052 .dispatch(&set(probe(ordered.iter().rev().cloned().collect())))
10053 .expect("a reordered list is valid");
10054 assert!(
10055 !state.diff(&reordered).is_empty(),
10056 "a header reorder is a cluster change"
10057 );
10058 assert!(state.diff(&state.clone()).is_empty());
10059
10060 let directory = tempfile::tempdir().expect("a temporary directory");
10062 let path = directory.path().join("headers.state");
10063 let mut file = File::create(&path).expect("creatable");
10064 let written = state.write_requests_to_file(&mut file).expect("saves");
10065 drop(file);
10066 let mut saved = Vec::new();
10067 File::open(&path)
10068 .expect("readable")
10069 .read_to_end(&mut saved)
10070 .expect("readable");
10071 let (remainder, requests) =
10072 parse_several_requests::<WorkerRequest>(&saved).expect("the file parses");
10073 assert!(remainder.is_empty());
10074 assert_eq!(requests.len(), written);
10075 let mut loaded = ConfigState::new();
10076 for request in &requests {
10077 loaded.dispatch(&request.content).expect("replays");
10078 }
10079
10080 let mut replayed = ConfigState::new();
10082 for request in state.generate_requests() {
10083 replayed.dispatch(&request).expect("replays");
10084 }
10085
10086 for (name, other) in [("loaded", &loaded), ("replayed", &replayed)] {
10087 assert_eq!(
10088 other.clusters["app"].health_check.as_ref().unwrap().headers,
10089 ordered,
10090 "{name} state must keep the exact ordered headers"
10091 );
10092 }
10093 }
10094
10095 #[test]
10100 fn protected_health_check_order_stores_metadata_only_and_refuses_before_mutation() {
10101 use crate::proto::command::{
10102 HealthCheckConfig, HealthCheckHeader, HealthCheckHeaderValue, HealthCheckMode,
10103 SetProtectedHealthCheck,
10104 };
10105
10106 const SECRET: &str = "SYNTHETIC-STATE-SECRET-77c1";
10107 let fingerprint = format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 1, 2);
10108 let order = |fingerprint: &str, carrier_name: &str| {
10109 RequestType::SetProtectedHealthCheck(SetProtectedHealthCheck {
10110 cluster_id: String::from("app"),
10111 config: HealthCheckConfig {
10112 uri: "/health".to_owned(),
10113 interval: 10,
10114 timeout: 5,
10115 healthy_threshold: 3,
10116 unhealthy_threshold: 3,
10117 mode: HealthCheckMode::Http as i32,
10118 headers: vec![HealthCheckHeader {
10119 name: "Authorization".to_owned(),
10120 fingerprint: fingerprint.to_owned(),
10121 }],
10122 ..Default::default()
10123 },
10124 values: vec![HealthCheckHeaderValue {
10125 name: carrier_name.to_owned(),
10126 value: SECRET.as_bytes().to_vec(),
10127 }],
10128 })
10129 .into()
10130 };
10131 let mut state = ConfigState::new();
10132 state
10133 .dispatch(
10134 &RequestType::AddCluster(Cluster {
10135 cluster_id: String::from("app"),
10136 ..Default::default()
10137 })
10138 .into(),
10139 )
10140 .expect("the cluster is added");
10141 let logical = |s: &ConfigState| {
10142 let mut c = s.clone();
10143 c.request_counts.clear();
10144 c
10145 };
10146
10147 let before = logical(&state);
10148 for (what, refused) in [
10149 ("no fingerprint", order("", "Authorization")),
10150 ("another carrier", order(&fingerprint, "X-Other")),
10151 ] {
10152 assert!(state.dispatch(&refused).is_err(), "{what}");
10153 assert_eq!(logical(&state), before, "{what}: no mutation");
10154 }
10155
10156 state
10157 .dispatch(&order(&fingerprint, "Authorization"))
10158 .expect("a fingerprinted order with its carrier is accepted");
10159 let stored = state.clusters["app"].health_check.as_ref().expect("set");
10160 assert_eq!(stored.headers[0].fingerprint, fingerprint);
10161 let rendered = format!("{state:?}{:?}", state.generate_requests());
10162 assert!(!rendered.contains(SECRET), "the state holds no value");
10163 let json = serde_json::to_string(&state.generate_requests()).expect("serializes");
10164 assert!(!json.contains(SECRET), "a replay holds no value");
10165 }
10166
10167 #[test]
10171 fn a_health_check_port_and_a_unix_backend_never_share_a_cluster() {
10172 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
10173
10174 let probe = |port| HealthCheckConfig {
10175 interval: 10,
10176 timeout: 5,
10177 healthy_threshold: 3,
10178 unhealthy_threshold: 3,
10179 mode: HealthCheckMode::Tcp as i32,
10180 port,
10181 ..Default::default()
10182 };
10183 let cluster = |health_check| -> Request {
10184 RequestType::AddCluster(Cluster {
10185 cluster_id: String::from("app"),
10186 health_check,
10187 ..Default::default()
10188 })
10189 .into()
10190 };
10191 let backend = |unix_path: Option<&str>| AddBackend {
10192 cluster_id: String::from("app"),
10193 backend_id: String::from("app-0"),
10194 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
10195 unix_path: unix_path.map(ToOwned::to_owned),
10196 ..Default::default()
10197 };
10198 let add_unix: Request = RequestType::AddUnixBackend(backend(Some("/run/app.sock"))).into();
10199 let set = |port| -> Request {
10200 RequestType::SetHealthCheck(SetHealthCheck {
10201 cluster_id: String::from("app"),
10202 config: probe(port),
10203 })
10204 .into()
10205 };
10206 let refused = |state: &mut ConfigState, request: &Request| {
10207 let before = (state.clusters.clone(), state.backends.clone());
10208 let error = state.dispatch(request).unwrap_err();
10209 assert!(
10210 matches!(
10211 error,
10212 StateError::InvalidValue {
10213 field: "health_check.port",
10214 reason: crate::config::HEALTH_CHECK_PORT_UNIX_REFUSED,
10215 }
10216 ),
10217 "expected the probe port / unix_path refusal, got: {error}"
10218 );
10219 assert_eq!(before, (state.clusters.clone(), state.backends.clone()));
10220 };
10221
10222 let mut state = ConfigState::new();
10224 state.dispatch(&cluster(Some(probe(Some(8081))))).unwrap();
10225 refused(&mut state, &add_unix);
10226 assert!(state.backends.get("app").is_none_or(Vec::is_empty));
10227 state
10228 .dispatch(&RequestType::AddBackend(backend(None)).into())
10229 .expect("an IP backend takes the probe port");
10230
10231 let mut state = ConfigState::new();
10234 state.dispatch(&cluster(Some(probe(None)))).unwrap();
10235 state.dispatch(&add_unix).unwrap();
10236 refused(&mut state, &set(Some(8081)));
10237 refused(&mut state, &cluster(Some(probe(Some(8081)))));
10238 assert_eq!(state.clusters["app"].health_check, Some(probe(None)));
10239 state
10240 .dispatch(&set(None))
10241 .expect("a unix backend is probed through its socket");
10242 state
10243 .dispatch(&cluster(None))
10244 .expect("an upsert without a health check keeps the unix backend");
10245
10246 let mut state = ConfigState::new();
10248 assert!(matches!(
10249 state.dispatch(&set(Some(8081))).unwrap_err(),
10250 StateError::NotFound { .. }
10251 ));
10252 assert!(matches!(
10253 state.dispatch(&add_unix).unwrap_err(),
10254 StateError::ClusterNotFound { .. }
10255 ));
10256 }
10257
10258 #[test]
10260 fn update_https_listener_alpn_unknown_value_rejected() {
10261 use crate::proto::command::AlpnProtocols;
10262
10263 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10264 let mut state = ConfigState::new();
10265 state
10266 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10267 .unwrap();
10268
10269 let patch = UpdateHttpsListenerConfig {
10270 address: addr,
10271 alpn_protocols: Some(AlpnProtocols {
10272 values: vec!["h3".to_owned()],
10273 }),
10274 ..Default::default()
10275 };
10276 let err = state
10277 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10278 .unwrap_err();
10279 assert!(
10280 matches!(
10281 err,
10282 StateError::InvalidValue {
10283 field: "alpn_protocols",
10284 ..
10285 }
10286 ),
10287 "expected InvalidValue for unknown ALPN, got: {err}"
10288 );
10289 }
10290
10291 #[test]
10293 fn update_https_listener_alpn_empty_reset_accepted() {
10294 use crate::proto::command::AlpnProtocols;
10295
10296 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10297 let mut state = ConfigState::new();
10298 let mut listener = make_https_listener(addr);
10299 listener.alpn_protocols = vec!["h2".to_owned()];
10300 state
10301 .dispatch(&RequestType::AddHttpsListener(listener).into())
10302 .unwrap();
10303
10304 let patch = UpdateHttpsListenerConfig {
10305 address: addr,
10306 alpn_protocols: Some(AlpnProtocols { values: vec![] }),
10307 ..Default::default()
10308 };
10309 state
10310 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10311 .expect("empty ALPN reset must succeed");
10312
10313 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10314 assert!(
10315 listener.alpn_protocols.is_empty(),
10316 "ALPN must have been reset to empty"
10317 );
10318 }
10319
10320 #[test]
10322 fn update_https_listener_alpn_valid_values_accepted() {
10323 use crate::proto::command::AlpnProtocols;
10324
10325 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10326 let mut state = ConfigState::new();
10327 state
10328 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10329 .unwrap();
10330
10331 let patch = UpdateHttpsListenerConfig {
10332 address: addr,
10333 alpn_protocols: Some(AlpnProtocols {
10334 values: vec!["h2".to_owned(), "http/1.1".to_owned()],
10335 }),
10336 ..Default::default()
10337 };
10338 state
10339 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10340 .expect("valid ALPN must be accepted");
10341
10342 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10343 assert_eq!(listener.alpn_protocols, vec!["h2", "http/1.1"]);
10344 }
10345
10346 #[test]
10349 fn update_https_listener_alpn_absent_preserves_existing() {
10350 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10351 let mut state = ConfigState::new();
10352 let mut listener = make_https_listener(addr);
10353 listener.alpn_protocols = vec!["h2".to_owned()];
10354 state
10355 .dispatch(&RequestType::AddHttpsListener(listener).into())
10356 .unwrap();
10357
10358 let patch = UpdateHttpsListenerConfig {
10360 address: addr,
10361 front_timeout: Some(10),
10362 ..Default::default()
10363 };
10364 state
10365 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10366 .unwrap();
10367
10368 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10369 assert_eq!(
10370 listener.alpn_protocols,
10371 vec!["h2"],
10372 "ALPN must be preserved when not patched"
10373 );
10374 }
10375
10376 #[test]
10378 fn update_https_listener_sozu_id_header_empty_rejected() {
10379 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10380 let mut state = ConfigState::new();
10381 state
10382 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10383 .unwrap();
10384
10385 let patch = UpdateHttpsListenerConfig {
10386 address: addr,
10387 sozu_id_header: Some(String::new()),
10388 ..Default::default()
10389 };
10390 let err = state
10391 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10392 .unwrap_err();
10393 assert!(
10394 matches!(
10395 err,
10396 StateError::InvalidValue {
10397 field: "sozu_id_header",
10398 ..
10399 }
10400 ),
10401 "expected InvalidValue for empty header name, got: {err}"
10402 );
10403 }
10404
10405 #[test]
10407 fn update_https_listener_sozu_id_header_colon_rejected() {
10408 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10409 let mut state = ConfigState::new();
10410 state
10411 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10412 .unwrap();
10413
10414 let patch = UpdateHttpsListenerConfig {
10415 address: addr,
10416 sozu_id_header: Some("bad: value".to_owned()),
10417 ..Default::default()
10418 };
10419 let err = state
10420 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10421 .unwrap_err();
10422 assert!(
10423 matches!(
10424 err,
10425 StateError::InvalidValue {
10426 field: "sozu_id_header",
10427 ..
10428 }
10429 ),
10430 "expected InvalidValue for header name with colon, got: {err}"
10431 );
10432 }
10433
10434 #[test]
10436 fn update_https_listener_sozu_id_header_valid_accepted() {
10437 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10438 let mut state = ConfigState::new();
10439 state
10440 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10441 .unwrap();
10442
10443 let patch = UpdateHttpsListenerConfig {
10444 address: addr,
10445 sozu_id_header: Some("X-Edge-Id".to_owned()),
10446 ..Default::default()
10447 };
10448 state
10449 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10450 .expect("valid header name must be accepted");
10451
10452 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10453 assert_eq!(listener.sozu_id_header.as_deref(), Some("X-Edge-Id"));
10454 }
10455
10456 #[test]
10462 fn add_listener_rejects_an_unknown_forwarded_headers_value() {
10463 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10464 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10465 let mut state = ConfigState::new();
10466
10467 let http_error = state
10468 .dispatch(
10469 &RequestType::AddHttpListener(HttpListenerConfig {
10470 forwarded_headers: Some(42),
10471 ..make_http_listener(http)
10472 })
10473 .into(),
10474 )
10475 .unwrap_err();
10476 let https_error = state
10477 .dispatch(
10478 &RequestType::AddHttpsListener(HttpsListenerConfig {
10479 forwarded_headers: Some(-1),
10480 ..make_https_listener(https)
10481 })
10482 .into(),
10483 )
10484 .unwrap_err();
10485 for error in [http_error, https_error] {
10486 assert!(
10487 matches!(
10488 error,
10489 StateError::InvalidValue {
10490 field: "forwarded_headers",
10491 ..
10492 }
10493 ),
10494 "expected InvalidValue for an unknown mode, got: {error}"
10495 );
10496 }
10497 assert!(state.http_listeners.is_empty() && state.https_listeners.is_empty());
10498
10499 state
10500 .dispatch(
10501 &RequestType::AddHttpListener(HttpListenerConfig {
10502 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10503 ..make_http_listener(http)
10504 })
10505 .into(),
10506 )
10507 .expect("a known mode must be accepted on an HTTP listener");
10508 state
10509 .dispatch(
10510 &RequestType::AddHttpsListener(HttpsListenerConfig {
10511 forwarded_headers: Some(ForwardedHeaders::None as i32),
10512 ..make_https_listener(https)
10513 })
10514 .into(),
10515 )
10516 .expect("a known mode must be accepted on an HTTPS listener");
10517 }
10518
10519 #[test]
10524 fn update_listener_forwarded_headers_applies_and_rejects_unknown_values() {
10525 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10526 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10527 let mut state = ConfigState::new();
10528 state
10529 .dispatch(&RequestType::AddHttpListener(make_http_listener(http)).into())
10530 .unwrap();
10531 state
10532 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https)).into())
10533 .unwrap();
10534
10535 state
10536 .dispatch(
10537 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10538 address: http,
10539 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10540 ..Default::default()
10541 })
10542 .into(),
10543 )
10544 .expect("a known mode must be accepted on an HTTP listener");
10545 state
10546 .dispatch(
10547 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10548 address: https,
10549 forwarded_headers: Some(ForwardedHeaders::None as i32),
10550 ..Default::default()
10551 })
10552 .into(),
10553 )
10554 .expect("a known mode must be accepted on an HTTPS listener");
10555 assert_eq!(
10556 state.http_listeners[&http.into()].forwarded_headers(),
10557 ForwardedHeaders::Rfc7239
10558 );
10559 assert_eq!(
10560 state.https_listeners[&https.into()].forwarded_headers(),
10561 ForwardedHeaders::None
10562 );
10563
10564 let http_error = state
10565 .dispatch(
10566 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10567 address: http,
10568 forwarded_headers: Some(42),
10569 ..Default::default()
10570 })
10571 .into(),
10572 )
10573 .unwrap_err();
10574 let https_error = state
10575 .dispatch(
10576 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10577 address: https,
10578 forwarded_headers: Some(-1),
10579 ..Default::default()
10580 })
10581 .into(),
10582 )
10583 .unwrap_err();
10584 for error in [http_error, https_error] {
10585 assert!(
10586 matches!(
10587 error,
10588 StateError::InvalidValue {
10589 field: "forwarded_headers",
10590 ..
10591 }
10592 ),
10593 "expected InvalidValue for an unknown mode, got: {error}"
10594 );
10595 }
10596 assert_eq!(
10598 state.http_listeners[&http.into()].forwarded_headers(),
10599 ForwardedHeaders::Rfc7239
10600 );
10601 assert_eq!(
10602 state.https_listeners[&https.into()].forwarded_headers(),
10603 ForwardedHeaders::None
10604 );
10605 }
10606
10607 #[test]
10609 fn update_https_listener_graceful_shutdown_zero_allowed() {
10610 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10611 let mut state = ConfigState::new();
10612 state
10613 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10614 .unwrap();
10615
10616 let patch = UpdateHttpsListenerConfig {
10617 address: addr,
10618 h2_graceful_shutdown_deadline_seconds: Some(0),
10619 ..Default::default()
10620 };
10621 state
10622 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10623 .expect("graceful_shutdown_deadline=0 must be allowed");
10624
10625 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10626 assert_eq!(listener.h2_graceful_shutdown_deadline_seconds, Some(0));
10627 }
10628
10629 #[test]
10633 fn update_http_listener_happy_path() {
10634 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10635 let mut state = ConfigState::new();
10636 state
10637 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10638 .unwrap();
10639
10640 let patch = UpdateHttpListenerConfig {
10641 address: addr,
10642 front_timeout: Some(15),
10643 h2_max_rst_stream_per_window: Some(25),
10644 ..Default::default()
10645 };
10646 state
10647 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10648 .expect("HTTP update must succeed");
10649
10650 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10651 assert_eq!(listener.front_timeout, 15);
10652 assert_eq!(listener.h2_max_rst_stream_per_window, Some(25));
10653 assert_eq!(listener.back_timeout, 30);
10655 }
10656
10657 #[test]
10664 fn update_http_listener_records_the_answers_map() {
10665 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10666 let mut state = ConfigState::new();
10667 let mut listener = make_http_listener(addr);
10668 listener.answers = [
10669 ("404".to_owned(), "stored 404".to_owned()),
10670 ("503".to_owned(), "stored 503".to_owned()),
10671 ]
10672 .into();
10673 state
10674 .dispatch(&RequestType::AddHttpListener(listener).into())
10675 .unwrap();
10676
10677 let patch = UpdateHttpListenerConfig {
10678 address: addr,
10679 answers: [
10680 ("404".to_owned(), String::new()),
10681 ("503".to_owned(), "patched 503".to_owned()),
10682 ("429".to_owned(), "patched 429".to_owned()),
10683 ]
10684 .into(),
10685 ..Default::default()
10686 };
10687 state
10688 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10689 .expect("HTTP update must succeed");
10690
10691 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10692 assert_eq!(
10693 listener.answers.get("404").map(String::as_str),
10694 Some("stored 404"),
10695 "an empty body preserves the stored template"
10696 );
10697 assert_eq!(
10698 listener.answers.get("503").map(String::as_str),
10699 Some("patched 503")
10700 );
10701 assert_eq!(
10702 listener.answers.get("429").map(String::as_str),
10703 Some("patched 429")
10704 );
10705 assert_eq!(listener.answers.len(), 3);
10706 }
10707
10708 #[test]
10714 fn update_https_listener_records_the_answers_map_and_hsts() {
10715 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10716 let mut state = ConfigState::new();
10717 let mut listener = make_https_listener(addr);
10718 listener.answers = [("404".to_owned(), "stored 404".to_owned())].into();
10719 listener.hsts = Some(HstsConfig {
10720 enabled: Some(true),
10721 max_age: Some(300),
10722 include_subdomains: Some(true),
10723 ..Default::default()
10724 });
10725 state
10726 .dispatch(&RequestType::AddHttpsListener(listener).into())
10727 .unwrap();
10728
10729 let disabled = HstsConfig {
10730 enabled: Some(false),
10731 ..Default::default()
10732 };
10733 let patch = UpdateHttpsListenerConfig {
10734 address: addr,
10735 answers: [
10736 ("404".to_owned(), String::new()),
10737 ("503".to_owned(), "patched 503".to_owned()),
10738 ]
10739 .into(),
10740 hsts: Some(disabled),
10741 ..Default::default()
10742 };
10743 state
10744 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10745 .expect("HTTPS update must succeed");
10746
10747 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10748 assert_eq!(
10749 listener.answers.get("404").map(String::as_str),
10750 Some("stored 404"),
10751 "an empty body preserves the stored template"
10752 );
10753 assert_eq!(
10754 listener.answers.get("503").map(String::as_str),
10755 Some("patched 503")
10756 );
10757 assert_eq!(listener.answers.len(), 2);
10758 assert_eq!(
10759 listener.hsts,
10760 Some(disabled),
10761 "the patched hsts block replaces the stored one whole"
10762 );
10763
10764 let patch = UpdateHttpsListenerConfig {
10765 address: addr,
10766 front_timeout: Some(15),
10767 ..Default::default()
10768 };
10769 state
10770 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10771 .expect("HTTPS update must succeed");
10772 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10773 assert_eq!(
10774 listener.hsts,
10775 Some(disabled),
10776 "a patch without hsts keeps the recorded policy"
10777 );
10778 }
10779
10780 #[test]
10782 fn update_http_listener_flood_knob_zero_rejected() {
10783 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10784 let mut state = ConfigState::new();
10785 state
10786 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10787 .unwrap();
10788
10789 let patch = UpdateHttpListenerConfig {
10790 address: addr,
10791 h2_max_window_update_stream0_per_window: Some(0),
10792 ..Default::default()
10793 };
10794 let err = state
10795 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10796 .unwrap_err();
10797 assert!(
10798 matches!(
10799 err,
10800 StateError::InvalidValue {
10801 field: "h2_max_window_update_stream0_per_window",
10802 ..
10803 }
10804 ),
10805 "expected InvalidValue, got: {err}"
10806 );
10807 }
10808
10809 #[test]
10815 fn update_http_listener_x_real_ip_patch_applied() {
10816 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10817 let mut state = ConfigState::new();
10818 state
10819 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10820 .unwrap();
10821
10822 let patch = UpdateHttpListenerConfig {
10823 address: addr,
10824 elide_x_real_ip: Some(true),
10825 send_x_real_ip: Some(true),
10826 ..Default::default()
10827 };
10828 state
10829 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10830 .expect("HTTP update must succeed");
10831
10832 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10833 assert_eq!(listener.elide_x_real_ip, Some(true));
10834 assert_eq!(listener.send_x_real_ip, Some(true));
10835 assert_eq!(listener.front_timeout, 60);
10837 }
10838
10839 #[test]
10843 fn update_http_listener_h2c_prior_knowledge_patch_applied_and_preserved() {
10844 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10845 let mut state = ConfigState::new();
10846 state
10847 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10848 .unwrap();
10849 assert_eq!(
10850 state
10851 .http_listeners
10852 .get(&ListenerKey::from(addr))
10853 .unwrap()
10854 .h2c_prior_knowledge,
10855 None
10856 );
10857
10858 let patch = UpdateHttpListenerConfig {
10859 address: addr,
10860 h2c_prior_knowledge: Some(true),
10861 ..Default::default()
10862 };
10863 state
10864 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10865 .expect("HTTP update must succeed");
10866 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10867 assert_eq!(listener.h2c_prior_knowledge, Some(true));
10868 assert_eq!(listener.front_timeout, 60);
10870
10871 let patch = UpdateHttpListenerConfig {
10872 address: addr,
10873 send_x_real_ip: Some(true),
10874 ..Default::default()
10875 };
10876 state
10877 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10878 .expect("HTTP update must succeed");
10879 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10880 assert_eq!(
10881 listener.h2c_prior_knowledge,
10882 Some(true),
10883 "a patch that does not name h2c_prior_knowledge must preserve it"
10884 );
10885 }
10886
10887 #[test]
10890 fn update_http_listener_x_real_ip_absent_preserves_existing() {
10891 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10892 let mut state = ConfigState::new();
10893 let listener = HttpListenerConfig {
10894 elide_x_real_ip: Some(true),
10895 send_x_real_ip: Some(false),
10896 ..make_http_listener(addr)
10897 };
10898 state
10899 .dispatch(&RequestType::AddHttpListener(listener).into())
10900 .unwrap();
10901
10902 let patch = UpdateHttpListenerConfig {
10903 address: addr,
10904 send_x_real_ip: Some(true),
10905 ..Default::default()
10906 };
10907 state
10908 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10909 .expect("HTTP update must succeed");
10910
10911 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10912 assert_eq!(listener.elide_x_real_ip, Some(true));
10913 assert_eq!(listener.send_x_real_ip, Some(true));
10914 }
10915
10916 #[test]
10919 fn update_https_listener_x_real_ip_patch_applied() {
10920 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10921 let mut state = ConfigState::new();
10922 state
10923 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10924 .unwrap();
10925
10926 let patch = UpdateHttpsListenerConfig {
10927 address: addr,
10928 elide_x_real_ip: Some(true),
10929 send_x_real_ip: Some(true),
10930 ..Default::default()
10931 };
10932 state
10933 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10934 .expect("HTTPS update must succeed");
10935
10936 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10937 assert_eq!(listener.elide_x_real_ip, Some(true));
10938 assert_eq!(listener.send_x_real_ip, Some(true));
10939 assert_eq!(listener.front_timeout, 60);
10941 }
10942
10943 #[test]
10946 fn update_https_listener_x_real_ip_absent_preserves_existing() {
10947 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10948 let mut state = ConfigState::new();
10949 let listener = HttpsListenerConfig {
10950 elide_x_real_ip: Some(false),
10951 send_x_real_ip: Some(true),
10952 ..make_https_listener(addr)
10953 };
10954 state
10955 .dispatch(&RequestType::AddHttpsListener(listener).into())
10956 .unwrap();
10957
10958 let patch = UpdateHttpsListenerConfig {
10959 address: addr,
10960 elide_x_real_ip: Some(true),
10961 ..Default::default()
10962 };
10963 state
10964 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10965 .expect("HTTPS update must succeed");
10966
10967 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10968 assert_eq!(listener.elide_x_real_ip, Some(true));
10969 assert_eq!(listener.send_x_real_ip, Some(true));
10970 }
10971
10972 #[test]
10975 fn update_listener_x_real_ip_survives_generate_requests_replay() {
10976 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10977 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10978 let mut state = ConfigState::new();
10979 state
10980 .dispatch(&RequestType::AddHttpListener(make_http_listener(http_addr)).into())
10981 .unwrap();
10982 state
10983 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https_addr)).into())
10984 .unwrap();
10985 state
10986 .dispatch(
10987 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10988 address: http_addr,
10989 elide_x_real_ip: Some(true),
10990 send_x_real_ip: Some(true),
10991 ..Default::default()
10992 })
10993 .into(),
10994 )
10995 .unwrap();
10996 state
10997 .dispatch(
10998 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10999 address: https_addr,
11000 elide_x_real_ip: Some(true),
11001 send_x_real_ip: Some(true),
11002 ..Default::default()
11003 })
11004 .into(),
11005 )
11006 .unwrap();
11007
11008 let mut replayed = ConfigState::new();
11009 for request in state.generate_requests() {
11010 replayed
11011 .dispatch(&request)
11012 .expect("could not replay generated request");
11013 }
11014
11015 let http = replayed
11016 .http_listeners
11017 .get(&ListenerKey::from(http_addr))
11018 .expect("replayed HTTP listener");
11019 assert_eq!(http.elide_x_real_ip, Some(true));
11020 assert_eq!(http.send_x_real_ip, Some(true));
11021 let https = replayed
11022 .https_listeners
11023 .get(&ListenerKey::from(https_addr))
11024 .expect("replayed HTTPS listener");
11025 assert_eq!(https.elide_x_real_ip, Some(true));
11026 assert_eq!(https.send_x_real_ip, Some(true));
11027 }
11028
11029 #[test]
11033 fn update_tcp_listener_happy_path() {
11034 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
11035 let mut state = ConfigState::new();
11036 state
11037 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
11038 .unwrap();
11039
11040 let patch = UpdateTcpListenerConfig {
11041 address: addr,
11042 front_timeout: Some(5),
11043 ..Default::default()
11044 };
11045 state
11046 .dispatch(&RequestType::UpdateTcpListener(patch).into())
11047 .expect("TCP update must succeed");
11048
11049 let listener = state.tcp_listeners.get(&ListenerKey::from(addr)).unwrap();
11050 assert_eq!(listener.front_timeout, 5);
11051 assert_eq!(listener.back_timeout, 30); }
11053
11054 #[test]
11056 fn update_tcp_listener_not_found() {
11057 let mut state = ConfigState::new();
11058 let patch = UpdateTcpListenerConfig {
11059 address: SocketAddress::new_v4(9, 9, 9, 9, 9999),
11060 front_timeout: Some(5),
11061 ..Default::default()
11062 };
11063 let err = state
11064 .dispatch(&RequestType::UpdateTcpListener(patch).into())
11065 .unwrap_err();
11066 assert!(
11067 matches!(
11068 err,
11069 StateError::NotFound {
11070 kind: ObjectKind::TcpListener,
11071 ..
11072 }
11073 ),
11074 "expected NotFound, got: {err}"
11075 );
11076 }
11077
11078 fn without_request_counts(state: &ConfigState) -> ConfigState {
11084 ConfigState {
11085 request_counts: BTreeMap::new(),
11086 ..state.clone()
11087 }
11088 }
11089
11090 #[test]
11101 fn update_http_listener_rejected_patch_leaves_the_state_unchanged() {
11102 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11103 let mut state = ConfigState::new();
11104 state
11105 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
11106 .unwrap();
11107
11108 let valid_prefix = UpdateHttpListenerConfig {
11109 address: addr,
11110 front_timeout: Some(99),
11111 sticky_name: Some("PATCHED".to_owned()),
11112 elide_x_real_ip: Some(true),
11113 http_answers: Some(CustomHttpAnswers {
11114 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
11115 ..Default::default()
11116 }),
11117 h2_max_rst_stream_per_window: Some(7),
11118 ..Default::default()
11119 };
11120 let rejected = [
11121 (
11122 "sozu_id_header",
11123 UpdateHttpListenerConfig {
11124 sozu_id_header: Some("bad header".to_owned()),
11125 ..valid_prefix.clone()
11126 },
11127 ),
11128 (
11129 "forwarded_headers",
11130 UpdateHttpListenerConfig {
11131 forwarded_headers: Some(i32::MAX),
11132 ..valid_prefix.clone()
11133 },
11134 ),
11135 ];
11136 for (field, patch) in rejected {
11137 let before = without_request_counts(&state);
11138 let err = state
11139 .dispatch(&RequestType::UpdateHttpListener(patch).into())
11140 .unwrap_err();
11141 assert!(
11142 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
11143 "expected InvalidValue on {field}, got: {err}"
11144 );
11145 assert!(
11146 without_request_counts(&state) == before,
11147 "a patch rejected on {field} must leave the main state unchanged"
11148 );
11149 }
11150
11151 state
11154 .dispatch(&RequestType::UpdateHttpListener(valid_prefix).into())
11155 .expect("the valid prefix alone must apply");
11156 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
11157 assert_eq!(listener.front_timeout, 99);
11158 assert_eq!(listener.sticky_name, "PATCHED");
11159 }
11160
11161 #[test]
11171 fn update_https_listener_rejected_patch_leaves_the_state_unchanged() {
11172 use crate::proto::command::AlpnProtocols;
11173
11174 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
11175 let mut state = ConfigState::new();
11176 state
11177 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
11178 .unwrap();
11179
11180 let valid_prefix = UpdateHttpsListenerConfig {
11181 address: addr,
11182 front_timeout: Some(99),
11183 sticky_name: Some("PATCHED".to_owned()),
11184 send_x_real_ip: Some(true),
11185 http_answers: Some(CustomHttpAnswers {
11186 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
11187 ..Default::default()
11188 }),
11189 strict_sni_binding: Some(false),
11190 h2_max_rst_stream_per_window: Some(7),
11191 ..Default::default()
11192 };
11193 let rejected = [
11194 (
11195 "alpn_protocols",
11196 UpdateHttpsListenerConfig {
11197 alpn_protocols: Some(AlpnProtocols {
11198 values: vec!["h3".to_owned()],
11199 }),
11200 ..valid_prefix.clone()
11201 },
11202 ),
11203 (
11204 "sozu_id_header",
11205 UpdateHttpsListenerConfig {
11206 sozu_id_header: Some(String::new()),
11207 ..valid_prefix.clone()
11208 },
11209 ),
11210 (
11211 "forwarded_headers",
11212 UpdateHttpsListenerConfig {
11213 forwarded_headers: Some(-1),
11214 ..valid_prefix.clone()
11215 },
11216 ),
11217 (
11218 "hsts",
11219 UpdateHttpsListenerConfig {
11220 hsts: Some(HstsConfig {
11221 enabled: None,
11222 max_age: Some(60),
11223 ..Default::default()
11224 }),
11225 ..valid_prefix.clone()
11226 },
11227 ),
11228 ];
11229 for (field, patch) in rejected {
11230 let before = without_request_counts(&state);
11231 let err = state
11232 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
11233 .unwrap_err();
11234 assert!(
11235 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
11236 "expected InvalidValue on {field}, got: {err}"
11237 );
11238 assert!(
11239 without_request_counts(&state) == before,
11240 "a patch rejected on {field} must leave the main state unchanged"
11241 );
11242 }
11243
11244 state
11245 .dispatch(&RequestType::UpdateHttpsListener(valid_prefix).into())
11246 .expect("the valid prefix alone must apply");
11247 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
11248 assert_eq!(listener.front_timeout, 99);
11249 assert_eq!(listener.strict_sni_binding, Some(false));
11250 }
11251
11252 #[test]
11256 fn update_tcp_listener_rejected_patch_leaves_the_state_unchanged() {
11257 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
11258 let mut state = ConfigState::new();
11259 state
11260 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
11261 .unwrap();
11262
11263 let before = without_request_counts(&state);
11264 let err = state
11265 .dispatch(
11266 &RequestType::UpdateTcpListener(UpdateTcpListenerConfig {
11267 address: SocketAddress::new_v4(0, 0, 0, 0, 9001),
11268 front_timeout: Some(5),
11269 ..Default::default()
11270 })
11271 .into(),
11272 )
11273 .unwrap_err();
11274 assert!(matches!(err, StateError::NotFound { .. }), "got: {err}");
11275 assert!(
11276 without_request_counts(&state) == before,
11277 "a refused TCP patch must change nothing"
11278 );
11279 }
11280
11281 #[test]
11287 fn dispatch_passes_through_set_metric_detail() {
11288 use crate::proto::command::{MetricDetail, SetMetricDetail};
11289 let mut state = ConfigState::new();
11290 let req: Request = RequestType::SetMetricDetail(SetMetricDetail {
11291 client_id: "test:1".to_owned(),
11292 detail: Some(MetricDetail::DetailBackend as i32),
11293 ttl_seconds: Some(60),
11294 clear: Some(false),
11295 reason: Some("regression-guard".to_owned()),
11296 peer_pid: None,
11297 peer_session_ulid: None,
11298 })
11299 .into();
11300 state
11301 .dispatch(&req)
11302 .expect("SetMetricDetail must traverse dispatch without UndispatchableRequest");
11303 }
11304
11305 #[test]
11344 fn remove_udp_frontend_drops_exactly_the_frontend_its_tags_name() {
11345 let team_a_address = SocketAddress::new_v4(127, 0, 0, 1, 9100);
11346 let team_b_address = SocketAddress::new_v4(127, 0, 0, 1, 9101);
11347 let front = |owner: &str, address| RequestUdpFrontend {
11348 cluster_id: "udp_cluster".to_string(),
11349 address,
11350 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
11351 };
11352 let mut state = ConfigState::new();
11353
11354 for (owner, address) in [("team-a", team_a_address), ("team-b", team_b_address)] {
11358 state
11359 .dispatch(&RequestType::AddUdpFrontend(front(owner, address)).into())
11360 .expect("one UDP frontend per address must be admitted");
11361 }
11362 assert_eq!(
11363 state.udp_fronts.get("udp_cluster").map(Vec::len),
11364 Some(2usize),
11365 "both tagged UDP frontends must be in the state before the removal"
11366 );
11367
11368 state
11369 .dispatch(&RequestType::RemoveUdpFrontend(front("team-a", team_a_address)).into())
11370 .expect("removing one of the two must succeed");
11371
11372 let surviving: Vec<&str> = state
11373 .udp_fronts
11374 .get("udp_cluster")
11375 .map(|fronts| {
11376 fronts
11377 .iter()
11378 .filter_map(|f| f.tags.get("owner").map(String::as_str))
11379 .collect()
11380 })
11381 .unwrap_or_default();
11382 assert_eq!(
11383 surviving,
11384 vec!["team-b"],
11385 "removing one UDP frontend must not evict the rest of its cluster's bucket"
11386 );
11387
11388 let tagless = RequestUdpFrontend {
11392 cluster_id: "udp_cluster".to_string(),
11393 address: team_b_address,
11394 tags: BTreeMap::new(),
11395 };
11396 let err = state
11397 .dispatch(&RequestType::RemoveUdpFrontend(tagless).into())
11398 .expect_err("a tagless remove must not match a tagged UDP frontend");
11399 assert!(
11400 matches!(err, StateError::NoChange),
11401 "expected NoChange, got: {err}"
11402 );
11403 assert_eq!(
11404 state.udp_fronts.get("udp_cluster").map(Vec::len),
11405 Some(1usize),
11406 "the tagless remove must leave the surviving tagged frontend in place"
11407 );
11408 }
11409
11410 fn udp_front(cluster_id: &str, address: SocketAddress, owner: &str) -> RequestUdpFrontend {
11411 RequestUdpFrontend {
11412 cluster_id: cluster_id.to_string(),
11413 address,
11414 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
11415 }
11416 }
11417
11418 #[test]
11449 fn add_udp_frontend_refuses_a_second_frontend_on_a_taken_address() {
11450 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11451 let other_address = SocketAddress::new_v4(127, 0, 0, 1, 5354);
11452 let mut state = ConfigState::new();
11453
11454 state
11455 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-a")).into())
11456 .expect("the first claim on an address must be admitted");
11457
11458 state
11460 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-b")).into())
11461 .expect_err("a second UDP frontend on a taken address must be refused");
11462
11463 state
11466 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11467 .expect_err("a second UDP frontend on a taken address must be refused");
11468
11469 assert_eq!(
11470 state.count_udp_frontends_raw(),
11471 1,
11472 "a refused claim must leave the state holding only the admitted frontend"
11473 );
11474 assert!(
11475 state.udp_fronts.get("syslog").is_none_or(Vec::is_empty),
11476 "a refused cross-cluster claim must not leave an opened bucket behind"
11477 );
11478
11479 state
11481 .dispatch(
11482 &RequestType::AddUdpFrontend(udp_front("syslog", other_address, "team-c")).into(),
11483 )
11484 .expect("a UDP frontend on a free address must still be admitted");
11485
11486 state
11489 .dispatch(&RequestType::RemoveUdpFrontend(udp_front("dns", address, "team-a")).into())
11490 .expect("removing the frontend holding the address must succeed");
11491 state
11492 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11493 .expect("a freed address must be claimable again, by any cluster");
11494
11495 assert_eq!(
11496 state.count_udp_frontends_raw(),
11497 2,
11498 "the freed address must now be held by exactly one frontend, beside the other address"
11499 );
11500 }
11501
11502 #[test]
11519 fn a_refused_udp_frontend_names_the_cluster_already_holding_the_address() {
11520 const OWNER_SECRET: &str = "UDP_ADDRESS_OWNER_SECRET_SENTINEL";
11521
11522 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11523 let owner_cluster = format!("{OWNER_SECRET}-dns");
11524 let owner_tag = format!("{OWNER_SECRET}-team-a");
11525 let mut state = ConfigState::new();
11526
11527 state
11528 .dispatch(
11529 &RequestType::AddUdpFrontend(udp_front(&owner_cluster, address, &owner_tag)).into(),
11530 )
11531 .expect("the first claim on an address must be admitted");
11532
11533 let error = state
11534 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11535 .expect_err("a second UDP frontend on a taken address must be refused");
11536
11537 match &error {
11538 StateError::UdpFrontendAddressTaken {
11539 address: refused_address,
11540 cluster_id,
11541 current_cluster_id,
11542 current_tags,
11543 } => {
11544 assert_eq!(
11545 *refused_address,
11546 SocketAddr::from(address),
11547 "the refusal must name the contested address"
11548 );
11549 assert_eq!(
11550 &**cluster_id, "syslog",
11551 "the refusal must name the claimant"
11552 );
11553 assert_eq!(
11554 **current_cluster_id, *owner_cluster,
11555 "the refusal must name the cluster already holding the address"
11556 );
11557 assert_eq!(
11558 current_tags.get("owner"),
11559 Some(&owner_tag),
11560 "the refusal must carry the current owner's access-log tags"
11561 );
11562 }
11563 other => panic!("expected UdpFrontendAddressTaken, got {other:?}"),
11564 }
11565
11566 for (label, output) in [
11569 ("Display", error.to_string()),
11570 ("Debug", format!("{error:?}")),
11571 ] {
11572 assert!(
11573 output.contains("127.0.0.1:5353"),
11574 "StateError {label} must name the contested address: {output}"
11575 );
11576 assert!(
11577 !output.contains(OWNER_SECRET),
11578 "StateError {label} leaked a protected identifier: {output}"
11579 );
11580 assert!(
11581 output.contains(&format!("current_cluster_id_bytes={}", owner_cluster.len())),
11582 "StateError {label} omitted the bounded owner metadata: {output}"
11583 );
11584 assert!(
11585 output.contains("current_tag_count=1"),
11586 "StateError {label} omitted the bounded owner tag count: {output}"
11587 );
11588 }
11589 }
11590
11591 #[test]
11614 fn a_saved_state_holding_one_udp_frontend_per_address_replays_unchanged() {
11615 use std::io::Read;
11616
11617 use crate::parser::parse_several_requests;
11618
11619 let first = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11620 let second = SocketAddress::new_v4(127, 0, 0, 2, 5353);
11621
11622 let mut state = ConfigState::new();
11623 for (cluster_id, address) in [("dns", first), ("syslog", second)] {
11624 state
11625 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
11626 .expect("could not add udp listener");
11627 state
11628 .dispatch(
11629 &RequestType::ActivateListener(ActivateListener {
11630 interface: None,
11631 address,
11632 proxy: ListenerType::Udp.into(),
11633 from_scm: false,
11634 })
11635 .into(),
11636 )
11637 .expect("could not activate udp listener");
11638 state
11639 .dispatch(
11640 &RequestType::AddUdpFrontend(udp_front(cluster_id, address, "team-a")).into(),
11641 )
11642 .expect("one UDP frontend per address must be admitted");
11643 }
11644
11645 let directory = tempfile::tempdir().expect("a temporary directory for the state file");
11646 let path = directory.path().join("udpaddr-roundtrip.state");
11647 let mut file = File::create(&path).expect("the state file must be creatable");
11648 let written = state
11649 .write_requests_to_file(&mut file)
11650 .expect("SaveState must serialise the state");
11651 drop(file);
11652
11653 let mut saved = Vec::new();
11654 File::open(&path)
11655 .expect("the state file must be readable")
11656 .read_to_end(&mut saved)
11657 .expect("the state file must be readable");
11658
11659 let (remainder, requests) =
11660 parse_several_requests::<WorkerRequest>(&saved).expect("the state file must parse");
11661 assert!(
11662 remainder.is_empty(),
11663 "load_state would report leftover bytes: {} unparsed",
11664 remainder.len()
11665 );
11666 assert_eq!(
11667 requests.len(),
11668 written,
11669 "every record write_requests_to_file emitted must parse back"
11670 );
11671
11672 let mut replayed = ConfigState::new();
11673 for request in &requests {
11674 replayed
11675 .dispatch(&request.content)
11676 .expect("every saved UDP entry must replay into a fresh state");
11677 }
11678
11679 let logical = |s: &ConfigState| {
11680 let mut c = s.clone();
11681 c.request_counts.clear();
11682 c
11683 };
11684 assert_eq!(
11685 logical(&state),
11686 logical(&replayed),
11687 "a SaveState -> LoadState round trip must reconstruct the UDP configuration"
11688 );
11689 }
11690
11691 fn with_interface<T>(mut listener: T, set: impl FnOnce(&mut T)) -> T {
11694 set(&mut listener);
11695 listener
11696 }
11697
11698 #[test]
11702 fn listeners_on_one_address_with_different_interfaces_coexist() {
11703 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
11704 let mut state = ConfigState::new();
11705 for interface in [None, Some("lo"), Some("wg0")] {
11706 let iface = interface.map(str::to_owned);
11707 state
11708 .dispatch(
11709 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11710 l.interface = iface.clone()
11711 }))
11712 .into(),
11713 )
11714 .expect("an HTTP listener on another interface must not collide");
11715 state
11716 .dispatch(
11717 &RequestType::AddHttpsListener(with_interface(
11718 make_https_listener(addr),
11719 |l| l.interface = iface.clone(),
11720 ))
11721 .into(),
11722 )
11723 .expect("an HTTPS listener on another interface must not collide");
11724 state
11725 .dispatch(
11726 &RequestType::AddTcpListener(with_interface(make_tcp_listener(addr), |l| {
11727 l.interface = iface.clone()
11728 }))
11729 .into(),
11730 )
11731 .expect("a TCP listener on another interface must not collide");
11732 state
11733 .dispatch(
11734 &RequestType::AddUdpListener(with_interface(
11735 make_udp_listener(addr, false),
11736 |l| l.interface = iface.clone(),
11737 ))
11738 .into(),
11739 )
11740 .expect("a UDP listener on another interface must not collide");
11741 }
11742
11743 let expected = vec![
11744 "0.0.0.0:8443".to_owned(),
11745 "0.0.0.0:8443%lo".to_owned(),
11746 "0.0.0.0:8443%wg0".to_owned(),
11747 ];
11748 let list = state.list_listeners();
11749 assert_eq!(
11750 list.http_listeners.keys().cloned().collect::<Vec<_>>(),
11751 expected
11752 );
11753 assert_eq!(
11754 list.https_listeners.keys().cloned().collect::<Vec<_>>(),
11755 expected
11756 );
11757 assert_eq!(
11758 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11759 expected
11760 );
11761 assert_eq!(
11762 list.udp_listeners.keys().cloned().collect::<Vec<_>>(),
11763 expected
11764 );
11765 assert_eq!(
11766 list.http_listeners["0.0.0.0:8443%wg0"].interface.as_deref(),
11767 Some("wg0")
11768 );
11769
11770 assert!(
11772 state
11773 .dispatch(
11774 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11775 l.interface = Some("lo".to_owned())
11776 }))
11777 .into(),
11778 )
11779 .is_err(),
11780 "the same (address, interface) must still be refused as a duplicate"
11781 );
11782
11783 state
11785 .dispatch(
11786 &RequestType::ActivateListener(ActivateListener {
11787 address: addr,
11788 proxy: ListenerType::Http.into(),
11789 from_scm: false,
11790 interface: Some("wg0".to_owned()),
11791 })
11792 .into(),
11793 )
11794 .expect("activating the wg0 listener");
11795 let list = state.list_listeners();
11796 assert!(list.http_listeners["0.0.0.0:8443%wg0"].active);
11797 assert!(!list.http_listeners["0.0.0.0:8443%lo"].active);
11798 assert!(!list.http_listeners["0.0.0.0:8443"].active);
11799
11800 state
11801 .dispatch(
11802 &RequestType::RemoveListener(RemoveListener {
11803 address: addr,
11804 proxy: ListenerType::Tcp.into(),
11805 interface: Some("lo".to_owned()),
11806 })
11807 .into(),
11808 )
11809 .expect("removing the lo TCP listener");
11810 let list = state.list_listeners();
11811 assert_eq!(
11812 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11813 vec!["0.0.0.0:8443".to_owned(), "0.0.0.0:8443%wg0".to_owned()]
11814 );
11815
11816 assert!(
11819 state
11820 .dispatch(
11821 &RequestType::RemoveListener(RemoveListener {
11822 address: addr,
11823 proxy: ListenerType::Tcp.into(),
11824 interface: Some("eth9".to_owned()),
11825 })
11826 .into(),
11827 )
11828 .is_err()
11829 );
11830
11831 let mut replayed = ConfigState::new();
11834 for request in state.generate_requests() {
11835 replayed
11836 .dispatch(&request)
11837 .expect("replaying generated requests");
11838 }
11839 assert_eq!(replayed.list_listeners(), state.list_listeners());
11840 assert!(state.diff(&replayed).is_empty());
11841 }
11842
11843 #[test]
11847 fn listener_diff_distinguishes_interfaces() {
11848 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11849 let mut before = ConfigState::new();
11850 before
11851 .dispatch(
11852 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11853 l.interface = Some("eth0".to_owned())
11854 }))
11855 .into(),
11856 )
11857 .unwrap();
11858 let mut after = ConfigState::new();
11859 after
11860 .dispatch(
11861 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11862 l.interface = Some("wg0".to_owned())
11863 }))
11864 .into(),
11865 )
11866 .unwrap();
11867
11868 let diff = before.diff(&after);
11869 let mut removed = Vec::new();
11870 let mut added = Vec::new();
11871 for request in &diff {
11872 match &request.request_type {
11873 Some(RequestType::RemoveListener(remove)) => removed.push(remove.interface.clone()),
11874 Some(RequestType::AddHttpListener(add)) => added.push(add.interface.clone()),
11875 _ => {}
11876 }
11877 }
11878 assert_eq!(removed, vec![Some("eth0".to_owned())]);
11879 assert_eq!(added, vec![Some("wg0".to_owned())]);
11880 }
11881
11882 #[test]
11888 fn reload_adding_an_interface_listener_sends_only_that_listener() {
11889 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11890 let front = RequestHttpFrontend {
11891 cluster_id: Some("cluster_1".to_owned()),
11892 hostname: "example.com".to_owned(),
11893 path: PathRule::prefix("/".to_owned()),
11894 address: addr,
11895 ..Default::default()
11896 };
11897 let mut before = ConfigState::new();
11898 for request in [
11899 RequestType::AddHttpListener(make_http_listener(addr)),
11900 RequestType::AddHttpFrontend(front),
11901 ] {
11902 before.dispatch(&request.into()).unwrap();
11903 }
11904 let mut after = before.clone();
11905 after
11906 .dispatch(
11907 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11908 l.interface = Some("wg0".to_owned())
11909 }))
11910 .into(),
11911 )
11912 .unwrap();
11913
11914 let diff = before.diff(&after);
11915 assert_eq!(diff.len(), 1, "{diff:?}");
11916 assert!(matches!(
11917 &diff[0].request_type,
11918 Some(RequestType::AddHttpListener(listener))
11919 if listener.interface.as_deref() == Some("wg0")
11920 ));
11921 }
11922
11923 #[test]
11927 fn update_listener_is_refused_when_the_address_is_ambiguous() {
11928 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11929 let mut state = ConfigState::new();
11930 state
11931 .dispatch(
11932 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11933 l.interface = Some("wg0".to_owned())
11934 }))
11935 .into(),
11936 )
11937 .unwrap();
11938
11939 state
11941 .dispatch(
11942 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11943 address: addr,
11944 front_timeout: Some(15),
11945 ..Default::default()
11946 })
11947 .into(),
11948 )
11949 .expect("an unambiguous patch applies to the interface-bound listener");
11950 assert_eq!(
11951 state.list_listeners().http_listeners["0.0.0.0:8080%wg0"].front_timeout,
11952 15
11953 );
11954
11955 state
11956 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
11957 .unwrap();
11958 let before = state.list_listeners();
11959 let error = state
11960 .dispatch(
11961 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11962 address: addr,
11963 front_timeout: Some(20),
11964 ..Default::default()
11965 })
11966 .into(),
11967 )
11968 .expect_err("a patch matching two listeners must be refused");
11969 assert!(
11970 matches!(error, StateError::AmbiguousListenerAddress { count: 2, .. }),
11971 "{error}"
11972 );
11973 assert_eq!(
11974 before,
11975 state.list_listeners(),
11976 "a refused patch must leave the listeners untouched"
11977 );
11978 }
11979
11980 #[test]
11986 fn header_predicates_are_part_of_the_frontend_state_key() {
11987 use crate::proto::command::{HeaderMatchKind, HeaderPredicate};
11988
11989 let predicate = |name: &str, kind: HeaderMatchKind, value: Option<&str>| HeaderPredicate {
11990 name: name.to_owned(),
11991 kind: kind as i32,
11992 value: value.map(str::to_owned),
11993 };
11994 let base = RequestHttpFrontend {
11995 cluster_id: Some("stable".to_owned()),
11996 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
11997 hostname: "api.example.com".to_owned(),
11998 path: PathRule::prefix("/".to_owned()),
11999 ..Default::default()
12000 };
12001 let canary = RequestHttpFrontend {
12002 cluster_id: Some("canary".to_owned()),
12003 header_predicates: vec![
12004 predicate("X-Canary", HeaderMatchKind::Exact, Some("always")),
12005 predicate("X-Beta", HeaderMatchKind::Present, None),
12006 ],
12007 ..base.clone()
12008 };
12009 let canary_respelled = RequestHttpFrontend {
12010 header_predicates: vec![
12011 predicate("x-beta", HeaderMatchKind::Present, None),
12012 predicate("x-canary", HeaderMatchKind::Exact, Some("always")),
12013 ],
12014 ..canary.clone()
12015 };
12016
12017 assert_eq!(
12018 base.to_string(),
12019 "127.0.0.1:8080;api.example.com;P/",
12020 "a frontend without predicates keeps its historical key"
12021 );
12022 assert_eq!(canary.to_string(), canary_respelled.to_string());
12023
12024 let single = RequestHttpFrontend {
12027 header_predicates: vec![predicate("X-Beta", HeaderMatchKind::Present, None)],
12028 ..base.clone()
12029 };
12030 let repeated = RequestHttpFrontend {
12031 header_predicates: vec![
12032 predicate("X-Beta", HeaderMatchKind::Present, None),
12033 predicate("x-beta", HeaderMatchKind::Present, None),
12034 ],
12035 ..base.clone()
12036 };
12037 assert_eq!(single.to_string(), repeated.to_string());
12038
12039 let mut state = ConfigState::default();
12040 state.add_http_frontend(&base).expect("base");
12041 state
12042 .add_http_frontend(&canary)
12043 .expect("a canary must coexist with its base");
12044 assert!(matches!(
12045 state.add_http_frontend(&canary_respelled),
12046 Err(StateError::Exists { .. })
12047 ));
12048 assert_eq!(state.http_fronts.len(), 2);
12049
12050 let mut replayed = ConfigState::default();
12053 for request in state.generate_requests() {
12054 replayed.dispatch(&request).expect("replay must succeed");
12055 }
12056 assert_eq!(replayed.http_fronts, state.http_fronts);
12057
12058 let replay_types: Vec<_> = state
12062 .generate_requests()
12063 .into_iter()
12064 .filter_map(|request| request.request_type)
12065 .collect();
12066 assert!(replay_types.contains(&RequestType::AddHttpFrontend(base.clone())));
12067 assert!(
12068 replay_types.contains(&RequestType::AddHttpFrontendWithHeaderPredicates(
12069 canary.clone()
12070 ))
12071 );
12072 assert!(!replay_types.contains(&RequestType::AddHttpFrontend(canary.clone())));
12073
12074 for add in [
12077 RequestType::AddHttpFrontend(canary.clone()),
12078 RequestType::AddHttpFrontendWithHeaderPredicates(canary.clone()),
12079 ] {
12080 assert_eq!(
12081 rollback_request(&add).and_then(|request| request.request_type),
12082 Some(RequestType::RemoveHttpFrontendWithHeaderPredicates(
12083 canary.clone()
12084 ))
12085 );
12086 }
12087 assert_eq!(
12088 rollback_request(&RequestType::AddHttpFrontend(base.clone()))
12089 .and_then(|request| request.request_type),
12090 Some(RequestType::RemoveHttpFrontend(base.clone()))
12091 );
12092
12093 state
12094 .remove_http_frontend(&canary_respelled)
12095 .expect("a respelled removal must find the canary");
12096 assert_eq!(
12097 state
12098 .http_fronts
12099 .values()
12100 .map(|front| front.cluster_id.as_deref())
12101 .collect::<Vec<_>>(),
12102 vec![Some("stable")],
12103 "removing the canary must leave its base"
12104 );
12105 }
12106
12107 #[test]
12112 fn close_sessions_is_never_saved_nor_replayed() {
12113 let mut state: ConfigState = Default::default();
12114 state
12115 .dispatch(
12116 &RequestType::AddCluster(Cluster {
12117 cluster_id: String::from("cluster_1"),
12118 ..Default::default()
12119 })
12120 .into(),
12121 )
12122 .expect("Could not execute request");
12123 state
12124 .dispatch(
12125 &RequestType::AddBackend(AddBackend {
12126 cluster_id: String::from("cluster_1"),
12127 backend_id: String::from("cluster_1-0"),
12128 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
12129 ..Default::default()
12130 })
12131 .into(),
12132 )
12133 .expect("Could not execute request");
12134 let requests_before = state.generate_requests();
12135 let hashes_before = state.hash_state();
12136
12137 state
12138 .dispatch(
12139 &RequestType::CloseSessions(CloseSessions {
12140 cluster_id: String::from("cluster_1"),
12141 backend_id: Some(String::from("cluster_1-0")),
12142 address: Some(SocketAddress::new_v4(127, 0, 0, 1, 1026)),
12143 })
12144 .into(),
12145 )
12146 .expect("CloseSessions must be accepted by the state");
12147
12148 let requests_after = state.generate_requests();
12149 assert_eq!(requests_after, requests_before);
12150 assert!(
12151 !requests_after
12152 .iter()
12153 .any(|request| matches!(request.request_type, Some(RequestType::CloseSessions(_))))
12154 );
12155 assert_eq!(state.hash_state(), hashes_before);
12156 }
12157}