Skip to main content

sozu_command_lib/
state.rs

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
38/// To use throughout Sōzu
39///
40/// `Arc`, not `Rc`, although each worker is single-threaded: embedders keep
41/// [`ConfigState`] and the `response` types behind locks shared across
42/// threads (a multi-threaded tokio runtime, axum extensions), so they must
43/// stay `Send + Sync`. The assertion below holds that contract.
44pub type ClusterId = std::sync::Arc<str>;
45
46// Public contract: every type carrying a `ClusterId` stays `Send + Sync`.
47// Switching the alias to `Rc<str>` fails to compile here rather than in an
48// embedder's build.
49const _: 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
60/// Build the unambiguous inverse of a mutating request.
61///
62/// The main process and each worker use the same inverse to restore their
63/// respective [`ConfigState`] when no runtime worker accepts a command. Only
64/// verbs whose prior value is fully determined by the request are covered.
65/// Upserts, listener updates, cluster removals, and certificate mutations need
66/// a snapshot of the displaced value and therefore return `None`.
67pub 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
109/// Why a UDP cluster and a backend with a `unix_path` never meet in the state.
110const 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    /// The object belongs to a cluster the state does not hold, never added
128    /// or already removed. A backend belongs to its cluster
129    /// (`ConfigState::add_backend`), so it is refused rather than stored
130    /// without one. Per `doc/observability.md` a cluster identifier is
131    /// protected material in error text: `Display`/`Debug` render only its
132    /// length, and the raw field stays lossless for callers.
133    #[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    /// Mirrors the worker's `ProxyError::InvalidTcpFrontend`
159    /// (`lib/src/tcp.rs`): the master rejects an `AddTcpFrontend` for the
160    /// same reasons `validate_new_tcp_front` would, so a route never lands
161    /// in `ConfigState` only to be NACKed by every worker on the next
162    /// fan-out (sozu-proxy/sozu#1290).
163    #[error("invalid TCP frontend for address {address}: reason_bytes={}", .reason.len())]
164    InvalidTcpFrontend { address: SocketAddr, reason: String },
165    /// A UDP listener address is the WHOLE routing key. A datagram carries no
166    /// SNI, no `Host` header and no path, so no attribute of an incoming
167    /// datagram could pick between two frontends bound to one address --
168    /// which is why this is not an unimplemented feature but an unanswerable
169    /// question, and why TCP legitimately hosts several frontends per address
170    /// while UDP cannot. The worker agrees by construction:
171    /// `UdpProxy::add_udp_front` (`lib/src/udp.rs`) overwrites
172    /// `listener.cluster_id` and the listener's tags, and
173    /// `cluster_for_listener` maps one listener token to exactly one cluster.
174    ///
175    /// `current_cluster_id` and `current_tags` name the frontend already
176    /// holding `address`, so a caller matching this variant can tell the
177    /// operator which frontend to remove first. Per `doc/observability.md`'s
178    /// sensitive-value logging boundary a cluster identifier and a frontend's
179    /// tags are protected material in error text, so the `Display`/`Debug`
180    /// projection renders only the address and bounded metadata; the raw
181    /// fields stay lossless for callers, exactly as that boundary specifies.
182    #[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    /// The `interface` of an added listener is not a valid interface name, or
196    /// the platform cannot bind a socket to an interface.
197    #[error("invalid listener interface: {0}")]
198    ListenerInterface(InterfaceError),
199    /// An `Update*Listener` patch names its listener by address only, and
200    /// several listeners share that address on different interfaces.
201    #[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
208/// Refuse a listener whose interface is not a valid name on this platform, so
209/// its key always round-trips through its text form (`ListenerKey`).
210fn 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
217/// The one listener an update patch names by `address`: the patch carries no
218/// interface, so it applies when exactly one listener sits on that address,
219/// whatever its interface, and is refused when several do.
220fn 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
259/// Why a cluster may not both name an affinity header or cookie and serve a
260/// TCP frontend.
261const 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/// Runtime selection metadata attached to one stored TLS certificate.
265///
266/// The certificate material remains in [`ConfigState::certificates`] so its
267/// public map type and every command protobuf stay unchanged. This sidecar is
268/// serialized with `ConfigState`: SaveState request generation, main-process
269/// upgrades and worker bootstrap can therefore retain an operator-provided
270/// expiration and the insertion order used to break equal-expiration ties.
271///
272/// Adding this public type and [`ConfigState::certificate_metadata`] is a Rust
273/// source compatibility change for downstream exhaustive `ConfigState`
274/// literals. Serialized states written before the field existed remain
275/// readable because the sidecar defaults to empty.
276#[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/// Metadata for every certificate on one listener address.
283///
284/// The serialized representation is transparently the fingerprint map. The
285/// private `next_order` cache is never trusted across deserialization; the
286/// first state-changing certificate mutation normalizes a legacy/partial
287/// index, then ordinary replay appends subsequent certificates in O(1).
288#[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/// The `ConfigState` represents the state of Sōzu's business, which is to forward traffic
305/// from frontends to backends. Hence, it contains all details about:
306///
307/// - listeners (socket addresses, for TCP and HTTP connections)
308/// - frontends (bind to a listener)
309/// - backends (to forward connections to)
310/// - clusters (routing rules from frontends to backends)
311/// - TLS certificates
312#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ConfigState {
314    pub clusters: BTreeMap<ClusterId, Cluster>,
315    pub backends: BTreeMap<ClusterId, Vec<Backend>>,
316    /// listener key (address, plus interface when bound) -> HTTP listener
317    pub http_listeners: BTreeMap<ListenerKey, HttpListenerConfig>,
318    /// listener key -> HTTPS listener
319    pub https_listeners: BTreeMap<ListenerKey, HttpsListenerConfig>,
320    /// listener key -> TCP listener
321    pub tcp_listeners: BTreeMap<ListenerKey, TcpListenerConfig>,
322    /// listener key -> UDP listener
323    pub udp_listeners: BTreeMap<ListenerKey, UdpListenerConfig>,
324    /// HTTP frontends, indexed by a summary of each front's address;hostname;path, for uniqueness.
325    /// For example: `"0.0.0.0:8080;lolcatho.st;P/api"`
326    pub http_fronts: BTreeMap<String, HttpFrontend>,
327    /// indexed by (address, hostname, path)
328    pub https_fronts: BTreeMap<String, HttpFrontend>,
329    pub tcp_fronts: HashMap<ClusterId, Vec<TcpFrontend>>,
330    pub udp_fronts: HashMap<ClusterId, Vec<UdpFrontend>>,
331    /// TLS certificate material by listener address and leaf fingerprint.
332    ///
333    /// Mutations must go through [`Self::dispatch`] so
334    /// [`Self::certificate_metadata`] remains coherent. The map stays public
335    /// for compatibility, but replacing entries directly in memory can
336    /// invalidate the sidecar without changing either map's cardinality; that
337    /// shape cannot be detected by the O(1) mutation fast path. Deserialized
338    /// state is treated as untrusted and normalized on its first successful
339    /// certificate mutation.
340    pub certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
341    /// Selection metadata for entries in `certificates`, keyed identically.
342    ///
343    /// A missing or partial sidecar is a valid legacy state. Read paths derive
344    /// a deterministic total order and certificate mutations normalize the
345    /// affected address only after all fallible validation has succeeded.
346    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
347    pub certificate_metadata: HashMap<SocketAddr, CertificateMetadataIndex>,
348    /// A census of requests that were received. Name of the request -> number of occurences
349    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    /// Certificates at `address` in their effective runtime insertion order.
406    ///
407    /// Legacy or externally deserialized states may have missing, duplicate or
408    /// gapped ordinals. They remain readable: entries without metadata sort
409    /// first by fingerprint; metadata-bearing entries follow by ordinal and
410    /// then fingerprint. The returned ordinals are always normalized to the
411    /// contiguous range `0..len`, without mutating `self`.
412    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    /// Replay requests for one listener, ordered exactly as its resolver was
458    /// populated and carrying every retained expiration override.
459    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    /// Take the canonical metadata index for an already-validated mutation.
496    ///
497    /// State built by this process carries a trusted `next_order` cache and
498    /// can be reused directly. Deserialized or partial state has no trusted
499    /// cache and pays the sorting cost once before the mutation commits.
500    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    /// Capture the compensating request that restores this exact state if
548    /// `request` is accepted here but refused by the runtime afterwards.
549    ///
550    /// Additions whose inverse is independent of prior state use
551    /// [`rollback_request`]. Frontend removals instead snapshot the stored
552    /// frontend before dispatch: their wire request is only a selector and
553    /// can omit metadata that an `Add` must restore. Listener activation also
554    /// snapshots the stored active flag so a refused activation only undoes
555    /// the transition from inactive to active.
556    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            // This is to avoid the error message. These request types are
713            // worker-only / runtime-only and do not affect the persisted
714            // ConfigState (e.g., a worker-side global limit set via
715            // SetMaxConnectionsPerIp does NOT survive a worker restart;
716            // operators must mirror the change in the TOML to make it
717            // sticky).
718            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            // an action on live sessions: never saved, never replayed
736            | RequestType::CloseSessions(_)
737            | RequestType::HardStop(_) => Ok(()),
738
739            _other_request => Err(StateError::UndispatchableRequest),
740        };
741
742        // Run-to-completion postcondition: whatever path `dispatch` took, the
743        // cross-map invariants of the model must hold once it returns. We run
744        // the full sweep on both success and error: a failed mutating handler
745        // (e.g. a duplicate `add_*` or an absent `remove_*`) is required to be
746        // a no-op, so the invariants must be intact regardless of the result.
747        #[cfg(debug_assertions)]
748        self.check_invariants();
749
750        result
751    }
752
753    /// Full cross-map invariant sweep for the control-plane state model.
754    ///
755    /// This is the run-to-completion postcondition called via a
756    /// `#[cfg(debug_assertions)]` guard at the end of [`Self::dispatch`]. It
757    /// encodes the coherence invariants the diff/replay machinery relies on:
758    /// every map entry is self-consistent (a value's stored key/cluster_id
759    /// matches the key it is filed under), and the public accounting helpers
760    /// (`count_frontends`/`count_backends`) agree with the raw map contents.
761    ///
762    /// Compiled out entirely in release builds (no body, no callers).
763    #[cfg(debug_assertions)]
764    fn check_invariants(&self) {
765        // Listener maps: the value's `address` and `interface` fields must
766        // match the `ListenerKey` it is filed under, or a hot-upgrade replay (which re-derives the
767        // key from the value) would land the entry under a different key.
768        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        // Clusters: the value's `cluster_id` must match the key it is filed
791        // under (replay re-keys on `cluster.cluster_id`).
792        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        // Backends: grouped by cluster_id. Every backend in a bucket must carry
801        // that bucket's cluster_id (replay groups on `backend.cluster_id`), and
802        // the per-cluster Vec must stay deduplicated on (backend_id, address) —
803        // `add_backend` upserts, so duplicates would mean lost state.
804        for (cluster_id, backends) in &self.backends {
805            // A backend belongs to its cluster: `add_backend` refuses one of
806            // an unknown cluster and `remove_cluster` removes the bucket.
807            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        // TCP frontends: grouped by cluster_id. Every frontend in a bucket must
829        // carry that bucket's cluster_id, and `add_tcp_frontend` rejects exact
830        // duplicates so each bucket stays a set.
831        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        // Certificates: nested map keyed by (address, fingerprint). We do not
847        // recompute each inner fingerprint here (expensive -- it requires
848        // re-parsing the stored PEM), but the outer bucket itself must never
849        // be empty. `add_certificate` and `replace_certificate` only ever
850        // materialise a bucket together with a certificate, and
851        // `remove_certificate` evicts the address key once its last
852        // certificate is gone -- so an address key mapped to zero
853        // certificates is unreachable through the public API. It is exactly
854        // the corrupt state sozu-proxy/sozu#1404 produced, and the same
855        // shape two OTHER paths were independently found to reach while
856        // adding this coverage: `remove_certificate` leaving a dangling
857        // empty bucket after evicting an address's last certificate
858        // (`remove_certificate_evicts_the_address_when_its_last_certificate_is_removed`),
859        // and a rejected `add_certificate` leaving one behind for a
860        // previously-absent address
861        // (`add_certificate_with_valid_pem_armor_but_invalid_x509_leaves_no_bucket`).
862        // Both are fixed alongside this check. Left uncaught, this state is
863        // invisible to `hash_state` (which does not fold `self.certificates`
864        // at all) and silently dropped by `generate_requests` (which emits no
865        // `AddCertificate` for an empty bucket) -- `check_invariants` is the
866        // one place in this postcondition sweep that can catch it.
867        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        // A trusted sidecar is produced only by successful certificate
874        // mutations in this module. Deserialization clears `next_order`, so
875        // legacy, partial and externally authored sidecars stay readable and
876        // are deliberately excluded until the next successful mutation
877        // normalizes that address.
878        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        // Public accounting helpers must agree with the raw maps. These are the
915        // numbers the CLI and metrics surface; a drift here is a real bug.
916        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    /// Raw count of TCP frontends across all clusters. Used by the debug-only
934    /// [`Self::check_invariants`] to cross-check `count_frontends`, and by
935    /// `add_tcp_frontend`'s no-mutation-before-admission accounting.
936    /// Compiled unconditionally (unlike `check_invariants`): it is a trivial
937    /// sum of bucket lengths, and `debug_assert!` arguments still typecheck
938    /// in release builds even though they compile out — a debug-gated helper
939    /// referenced there breaks the release/bench build.
940    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    /// Frontends (every protocol) and backends whose owning cluster satisfies
949    /// `owned`. A frontend without a `cluster_id` is never counted. Used by
950    /// `remove_cluster`'s assertions, and compiled unconditionally for the
951    /// same reason as [`Self::count_tcp_frontends_raw`].
952    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    /// Debug builds only: it walks every frontend and backend, which a
978    /// release `remove_cluster` must not pay for.
979    #[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    /// Increments the count for this request type
985    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        // Validate any inline `cluster.health_check` before mutating state so
1003        // an invalid config (zero thresholds, missing leading `/`, CR/LF/NUL/C0
1004        // in URI) cannot ride in via the AddCluster path. Without this, TOML
1005        // reload, SaveState/LoadState, and direct API AddCluster requests
1006        // bypass the SetHealthCheck-side check and let an attacker-controlled
1007        // health-check URI smuggle CRLF into outbound HTTP/1.1 probes.
1008        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        // A cluster that turns UDP cannot keep a backend dialed over a UNIX
1017        // stream socket (see `add_backend`).
1018        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        // Same gate for the client affinity key: a name no request can carry,
1030        // or both a header and a cookie, would key selection on something
1031        // other than what the cluster declares.
1032        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        // A TCP session has no request to read a header or a cookie from and
1061        // always keys on the source IP, so a cluster serving TCP frontends
1062        // may not name one: the configuration would claim a key the proxy
1063        // does not use. `add_tcp_frontend` holds the other order.
1064        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        // AddCluster is an upsert (replacing an existing cluster_id keeps the
1077        // entry count flat), so we assert on presence/key-coherence rather than
1078        // a strict +1 on len.
1079        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    /// Remove a cluster together with every frontend and backend that names
1096    /// it, for every protocol. A frontend without a `cluster_id` (a deny or
1097    /// answer route) names no cluster and stays. A cluster the state does not
1098    /// hold is `NotFound`, and then nothing is removed, not even a frontend
1099    /// that names it.
1100    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    /// The removal orders for every frontend and backend
1152    /// `ConfigState::remove_cluster` would remove with `cluster_id`, or none when
1153    /// the state does not hold that cluster. A worker reads them from its own
1154    /// state before applying a `RemoveCluster` to it, then applies them to
1155    /// its proxies so they drop what the state drops.
1156    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    /// Whether `error`, the answer [`Self::dispatch`] gave to `request`,
1211    /// means the frontend or backend `request` removes already went away
1212    /// with its cluster: the removal found nothing to remove (`NotFound`),
1213    /// and the state does not hold the cluster it names either, so
1214    /// `RemoveCluster` removed it. Such a removal has nothing left to do and
1215    /// is answered ok, which keeps `sozu cluster remove` followed by the
1216    /// removal of each of its frontends working. A frontend without a
1217    /// `cluster_id` names no cluster and never matches.
1218    ///
1219    /// It never covers an object another cluster owns: a frontend removal
1220    /// matches only a frontend of the cluster it names (see
1221    /// `remove_frontend_from`), so its `NotFound` leaves such an object alone.
1222    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    /// The answer to a removal [`Self::removed_with_its_cluster`] accepts.
1242    /// The same answer covers a cluster that never existed, a typo for
1243    /// instance, so it says the cluster does not exist and, when another
1244    /// cluster (or a route with no cluster) now holds the same key or
1245    /// address, which one: that object is not removed.
1246    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    /// The metadata half of a protected order, whose values the state never
1311    /// holds. The order must be the main process's own: every header carries a
1312    /// fingerprint, and the carriers match the headers one for one. The state
1313    /// keeps names and fingerprints only.
1314    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        // Validate before mutating state so an invalid config (zero
1336        // thresholds, missing leading `/`, CR/LF/NUL/C0 in URI) cannot
1337        // round-trip through SaveState/LoadState. The worker also
1338        // validates at the SetHealthCheck handler — this is the
1339        // master-side mirror so off-channel TOML reload paths don't
1340        // bypass the policy.
1341        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        // Refuse an unknown mode before storing anything: the worker would
1389        // otherwise read it as the default while the state records it.
1390        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        // Refuse an unknown mode before storing anything: the worker would
1423        // otherwise read it as the default while the state records it.
1424        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    /// Validate and apply a partial patch to an existing HTTP listener.
1581    ///
1582    /// Only `Some` fields in the patch are written; `None` fields preserve the
1583    /// current value. Returns `StateError::NotFound` if the address is unknown,
1584    /// `StateError::InvalidValue` if a flood-knob value is below the required
1585    /// minimum or another field is invalid.
1586    ///
1587    /// All-or-nothing (sozu-proxy/sozu#1703): every fallible check runs before
1588    /// the first write, so a refused patch leaves the stored listener exactly
1589    /// as it was. Every write below is infallible; keep it that way, or move
1590    /// a new check up with the others.
1591    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        // Shared session-at-accept / per-connection knobs
1612        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        // H2 flood knobs
1647        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        // 0 is valid for graceful_shutdown_deadline (means "wait forever")
1699        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    /// The patch a configuration reload sends for an HTTPS listener that is
1715    /// already running with another client authentication policy.
1716    ///
1717    /// A reload replays `AddHttpsListener` for every listener of the file,
1718    /// and the state refuses it for an address it already holds, so a CA or
1719    /// CRL refreshed on disk would never reach the workers. With revocation
1720    /// expiry enforced, a CRL that could not be refreshed would end up
1721    /// rejecting every client. `None` when no listener is at that key, or
1722    /// when its policy already matches.
1723    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    /// Validate and apply a partial patch to an existing HTTPS listener.
1737    ///
1738    /// Only `Some` fields in the patch are written; `None` fields preserve the
1739    /// current value. Returns `StateError::NotFound` if the address is unknown,
1740    /// `StateError::InvalidValue` if a flood-knob value is below the required
1741    /// minimum, an ALPN value is unknown, an `hsts` block lacks `enabled`, or
1742    /// another field is invalid.
1743    ///
1744    /// All-or-nothing (sozu-proxy/sozu#1703), as
1745    /// [`Self::update_http_listener`]: every fallible check runs before the
1746    /// first write.
1747    ///
1748    /// `hsts` is recorded as a full-object replacement, as the workers apply
1749    /// it, so `SaveState`, `ListListeners` and the replay to a new worker
1750    /// carry the patched listener default (sozu-proxy/sozu#1715). The
1751    /// `enabled` check makes the main process refuse the same patch every
1752    /// worker refuses (`ListenerError::HstsEnabledRequired` in
1753    /// `HttpsListener::update_config`, `lib/src/https.rs`) instead of
1754    /// recording its fields while no worker applies them.
1755    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        // Shared session-at-accept / per-connection knobs
1785        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        // HTTPS-only knobs
1817        // Full-object replacement, as the worker applies it.
1818        if let Some(hsts) = patch.hsts {
1819            listener.hsts = Some(hsts);
1820        }
1821        if let Some(ref alpn_wrapper) = patch.alpn_protocols {
1822            // Empty values vec = reset to default (runtime treats empty as default)
1823            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        // H2 flood knobs
1835        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        // 0 is valid for graceful_shutdown_deadline (means "wait forever")
1887        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    /// Validate and apply a partial patch to an existing TCP listener.
1903    ///
1904    /// Only `Some` fields in the patch are written; `None` fields preserve the
1905    /// current value. Returns `StateError::NotFound` if the address is unknown.
1906    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    /// Validate and apply a partial patch to an existing UDP listener.
1936    ///
1937    /// Only `Some` fields in the patch are written; `None` fields preserve the
1938    /// current value. Returns `StateError::NotFound` if the address is unknown.
1939    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    /// Insert `front` under its route key in `fronts`, or refuse a duplicate.
2043    ///
2044    /// The HTTP and HTTPS frontend adds differ only by the map they index and
2045    /// the [`ObjectKind`] they report, so they share this body: a second copy
2046    /// of the admission logic is a second place for the route-key identity to
2047    /// drift.
2048    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        // On the conversion-error path the `?` already returned, so reaching
2078        // here means exactly one entry was inserted under the route key.
2079        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    /// Evict `front`'s route key from `fronts`. Shared by the HTTP and HTTPS
2092    /// frontend removals for the same reason [`Self::add_frontend_to`] is.
2093    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        // The route key names no cluster: a key freed by one cluster's
2101        // removal can be taken by another. Match the stored frontend only if
2102        // it belongs to the cluster the removal names, or a late removal for
2103        // the first cluster would evict the second one's route.
2104        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        // Duplicate AddCertificate is historically a complete no-op keyed by
2152        // leaf fingerprint. In particular it must not rewrite material,
2153        // expiration metadata or selection order from a differing retry.
2154        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        // Both fallible steps (`fingerprint()` above, `apply_overriding_names`
2175        // below -- the latter re-parses as X.509 when `names` is empty, so
2176        // well-formed PEM armor around invalid DER content still fails here)
2177        // run BEFORE `self.certificates` is touched. `entry(..).or_default()`
2178        // unconditionally materialises a bucket for the address, including a
2179        // brand-new one; ordering it after either fallible step used to leave
2180        // a dangling empty bucket for a previously-absent address on a
2181        // rejected add -- the same "mutate before the fallible step" defect
2182        // class as sozu-proxy/sozu#1404's `replace_certificate`, found while
2183        // adding certificate coverage to `check_invariants` for that issue.
2184        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            // The existing index came from a legacy or externally
2215            // deserialized state. Validation above has completed, so it is
2216            // now safe to normalize once and commit the new entry.
2217            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        // Mirror `CertificateResolver::remove_certificate`'s
2272        // `name_fingerprint_idx` cleanup (`lib/src/tls.rs`, #1202): an
2273        // address bucket emptied by removing its last certificate must not
2274        // linger as a dangling `self.certificates[address] == {}` entry.
2275        // That leftover was reachable through ordinary
2276        // `AddCertificate`+`RemoveCertificate` and broke
2277        // `generate_requests`'s own round-trip postcondition (an empty
2278        // bucket contributes zero `AddCertificate` requests, so replaying
2279        // them into a fresh `ConfigState` never recreates the entry).
2280        // Found while adding certificate coverage to `check_invariants` for
2281        // sozu-proxy/sozu#1404.
2282        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    /// - check that the address is known
2293    /// - calculate the new fingerprint (fallible: parses the PEM)
2294    /// - insert the new certificate with the new fingerprint as key
2295    /// - check that the new entry is present in the certificates hashmap
2296    /// - only then remove the old certificate, using the old fingerprint
2297    ///
2298    /// Add-before-remove ordering, mirroring the fix applied to
2299    /// `CertificateResolver::replace_certificate` in `lib/src/tls.rs` for
2300    /// the same defect (sozu-proxy/sozu#1202, closing #774): computing the
2301    /// new fingerprint is fallible, so nothing is removed until the
2302    /// replacement is safely stored. A malformed PEM therefore leaves the
2303    /// address's certificate map untouched instead of emptied
2304    /// (sozu-proxy/sozu#1404). This also makes the idempotent case (old
2305    /// fingerprint == new fingerprint, e.g. an operator or ACME retry loop
2306    /// resubmitting the same PEM) naturally safe: the entry is
2307    /// (re-)inserted and then deliberately NOT removed, instead of being
2308    /// removed right after being re-inserted under the same key.
2309    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        // A byte-for-byte effective self-replace is a complete no-op. Return
2345        // before normalizing legacy metadata so successful no-op commands keep
2346        // their historical behavior and cost.
2347        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        // Postcondition: the new fingerprint is keyed under the address, and
2392        // (unless old and new collide, e.g. a self-replace) the old one is gone.
2393        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    /// Admission control mirrors the worker's `validate_new_tcp_front`
2411    /// (`lib/src/tcp.rs`) so the master never persists a route its own
2412    /// workers reject on the next fan-out (sozu-proxy/sozu#1290): the
2413    /// command server mutates master state before dispatching to workers,
2414    /// and a worker NACK never rolls the master back. Every check below
2415    /// runs, and every canonicalization is computed, BEFORE `self.tcp_fronts`
2416    /// is touched -- an early `Err` return is a true no-op on `self`.
2417    fn add_tcp_frontend(&mut self, front: &RequestTcpFrontend) -> Result<(), StateError> {
2418        let address: SocketAddr = front.address.into();
2419
2420        // The other order of `add_cluster`'s check: a TCP frontend may not
2421        // join a cluster that keys on a request header or cookie.
2422        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        // Canonicalize first: shape-validate and lowercase `sni` through the
2436        // SAME `validate_sni_pattern` config-load and the worker use, and
2437        // reduce `alpn` to its canonical sorted+deduped set. Both canonical
2438        // forms are what gets stored (so the state's own hashing/diffing/
2439        // `generate_requests` replay sees one identity per route,
2440        // independent of how the caller cased/ordered its request) AND what
2441        // every admission check below compares against.
2442        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        // Worker parity (`validate_new_tcp_front`'s first no-SNI check):
2454        // alpn only matches within an SNI-scoped preread, so a frontend
2455        // without sni would silently ignore its alpn list. Config-load's
2456        // `to_tcp_front` already rejects this shape; a raw `AddTcpFrontend`
2457        // over the command socket must not slip past the master either.
2458        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        // Listener-wide scan across ALL clusters' buckets at this address --
2471        // the previous check only walked `front.cluster_id`'s own bucket, so
2472        // a duplicate identity, a no-SNI/SNI mix, a second catch-all, or an
2473        // overlapping ALPN set under a DIFFERENT cluster_id all used to pass
2474        // here and then fail on every worker.
2475        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                    // Catch-all / ALPN-overlap only matter between two
2497                    // SNI-scoped fronts sharing the exact same normalized
2498                    // sni -- exact and wildcard patterns are DISTINCT keys
2499                    // here, never matched against each other (no trie
2500                    // matching in `ConfigState`), mirroring the worker's own
2501                    // `accept_wildcard: false` self-lookup.
2502                    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                    // A catch-all coexisting with an ALPN-scoped sibling on
2518                    // the same sni is legal (worker parity: `AlpnMatcher::Any`
2519                    // only conflicts with another `Any`, never with a
2520                    // `OneOf`) -- only two ALPN-scoped fronts can overlap.
2521                    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        // POST: every admission check above either returned `Err` or fell
2559        // through -- none of them may mutate `self`, so the total frontend
2560        // count observed before the scan must still hold at this point.
2561        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        // Canonicalize for matching, mirroring add_tcp_frontend and the
2615        // worker: lowercase `sni` (the SNI-preread core normalizes to
2616        // lowercase before ever looking a route up, and `TcpFrontend.sni` is
2617        // stored in that same normalized form) and reduce `alpn` to its
2618        // canonical sorted+deduped set, so a request differing only in sni
2619        // case or alpn order still matches the stored frontend instead of
2620        // falling through to `NoChange`. Shape is intentionally NOT
2621        // re-validated here: a malformed pattern could never have been
2622        // stored post-fix, so it simply matches nothing below.
2623        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        // INV: removal identity mirrors add_tcp_frontend's (address, sni,
2626        // alpn) key — removing one SNI-scoped frontend on a listener must
2627        // not also evict a sibling frontend at the same address with a
2628        // different sni/alpn.
2629        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        // `retain` may drop more than one entry only if duplicates on the same
2637        // (address, sni, alpn) ever existed; `add_tcp_frontend` forbids that,
2638        // so a successful removal must drop exactly one and leave none
2639        // matching.
2640        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    /// Admits at most ONE UDP frontend per address, across every cluster.
2658    ///
2659    /// UDP has no routing discriminator: a datagram carries no SNI, no `Host`
2660    /// header and no path, so the listener address IS the entire routing key
2661    /// and no attribute of an incoming datagram could pick between two
2662    /// frontends bound to it. TCP legitimately hosts several frontends per
2663    /// address because `(address, sni, alpn)` discriminates; UDP has no
2664    /// equivalent.
2665    ///
2666    /// The worker cannot represent a second claim either:
2667    /// `UdpProxy::add_udp_front` (`lib/src/udp.rs`) overwrites
2668    /// `listener.cluster_id` and `listener.set_tags(address, ...)`, and
2669    /// `cluster_for_listener: HashMap<Token, ClusterId>` holds strictly one
2670    /// cluster per listener token, one listener per address. So the state used
2671    /// to admit two shapes the datapath silently collapsed to whichever
2672    /// frontend was applied LAST: two frontends at one (cluster, address)
2673    /// differing only in their tags, and -- with no check at all, because the
2674    /// scan never left `front.cluster_id`'s own bucket -- two DIFFERENT
2675    /// clusters claiming one address.
2676    ///
2677    /// Both are refused here instead, which is the same listener-wide
2678    /// admission [`Self::add_tcp_frontend`] performs for its own key. Every
2679    /// check runs BEFORE `self.udp_fronts` is touched, so an early `Err`
2680    /// return is a true no-op on `self` -- the `entry(...).or_default()` that
2681    /// used to open the bucket first now runs only once admission has passed.
2682    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        // Listener-wide scan across ALL clusters' buckets at this address, as
2704        // `add_tcp_frontend` does: the previous check only walked
2705        // `front.cluster_id`'s own bucket, so a second cluster claiming the
2706        // same address never met a check at all.
2707        if let Some(current) = self
2708            .udp_fronts
2709            .values()
2710            .flatten()
2711            .find(|existing| existing.address == address)
2712        {
2713            // An exact re-add of the identity `add_udp_frontend` already
2714            // admitted stays `Exists`, the answer every other add gives for
2715            // its own key and the one whose message tells the operator to
2716            // remove it first or send the corresponding update.
2717            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        // POST: every admission check above either returned `Err` or fell
2732        // through -- none of them may mutate `self`, so the total frontend
2733        // count observed before the scan must still hold at this point.
2734        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        // INV: a UDP listener address is the ENTIRE routing key, so exactly
2752        // one frontend may hold it across every cluster -- the model the
2753        // worker's one-cluster-per-listener-token datapath can represent.
2754        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        // INV: removal identity mirrors add_udp_frontend's
2781        // (cluster_id, address, tags) key -- the bucket this `get_mut`
2782        // returned already scopes `cluster_id`, exactly as
2783        // `tcp_frontend_matches` leaves it to `remove_tcp_frontend`'s own
2784        // bucket lookup. The tags stay load-bearing now that
2785        // `add_udp_frontend` admits one frontend per address across every
2786        // cluster: a remove that omits them matches only a frontend stored
2787        // with no tags, so against a TAGGED frontend on that address it must
2788        // answer `NoChange` rather than clear it.
2789        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        // `retain` may drop more than one entry only if duplicates on the same
2798        // (cluster_id, address, tags) ever existed; `add_udp_frontend` forbids
2799        // that, so a successful removal must drop exactly one and leave none
2800        // matching.
2801        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    /// Why `request` would put a health check `port` next to a backend with a
2814    /// `unix_path` in one cluster, which
2815    /// `sozu_command::config::HEALTH_CHECK_PORT_UNIX_REFUSED` forbids, in either order of arrival: the port set on a cluster that
2816    /// holds a unix backend (`AddCluster`, `SetHealthCheck`), or a unix backend
2817    /// added to a cluster whose health check sets a port.
2818    ///
2819    /// `None` for every other request, and for one a more specific refusal
2820    /// already covers (an unknown cluster, an invalid health check, a UDP
2821    /// cluster, a `unix_path` that does not parse), so those diagnostics keep
2822    /// their precedence. The worker asks this before dispatching, because it
2823    /// must not touch its proxies for a request the state refuses.
2824    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    /// Whether datagrams, not streams, reach this cluster: it has a UDP
2869    /// frontend or carries UDP-specific settings.
2870    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    /// Add or update a backend of an existing cluster. A backend belongs to
2881    /// its cluster: one naming a cluster the state does not hold, never added
2882    /// or already removed (`Self::remove_cluster` removes its backends), is
2883    /// refused with `ClusterNotFound` and the state is left untouched, before
2884    /// any `unix_path` or UDP validation.
2885    /// Frontends stay independent of their cluster's presence.
2886    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            // A UDP cluster relays datagrams over UDP sockets: a UNIX stream
2907            // endpoint cannot carry them, and the datapath would relay to the
2908            // identity `address` instead. The frontend that makes the cluster
2909            // UDP may land after the backend; `add_udp_frontend` refuses that
2910            // order.
2911            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        // we might be modifying the sticky id or load balancing parameters:
2933        // the retain drops at most one prior copy (the map stays deduplicated
2934        // on (backend_id, address)), then we re-push the new version. So the
2935        // net length grows by exactly one iff this was a brand-new backend.
2936        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        // The list is deduplicated on (backend_id, address), so a matching
2982        // removal drops exactly one entry and leaves nothing matching.
2983        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    /// creates all requests needed to bootstrap the state
2994    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        // Bootstrap round-trip: replaying `generate_requests` into a fresh,
3092        // empty `ConfigState` must reconstruct `self`'s maps exactly. This is
3093        // the property SaveState/LoadState and worker bootstrap depend on. We
3094        // compare the business maps only (not `request_counts`, which is
3095        // bookkeeping mutated by `dispatch`).
3096        #[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        // Symmetry with the active-listener census: exactly one ActivateListener
3192        // is emitted per active listener across the four maps, no more, no less.
3193        #[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                // any added listener should be unactive
3407                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                // The Remove + Add(active=false) above wipes the listener's
3412                // active state. Re-emit an ActivateListener whenever the target
3413                // state keeps it active, otherwise a config change on a still
3414                // active listener would silently deactivate it on replay. This
3415                // subsumes the newly-active (`!my.active && their.active`) case:
3416                // a differing `active` flag always makes the listeners unequal.
3417                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                // any added listener should be unactive
3457                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                // The Remove + Add(active=false) above wipes the listener's
3462                // active state. Re-emit an ActivateListener whenever the target
3463                // state keeps it active, otherwise a config change on a still
3464                // active listener would silently deactivate it on replay. This
3465                // subsumes the newly-active (`!my.active && their.active`) case:
3466                // a differing `active` flag always makes the listeners unequal.
3467                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                // any added listener should be unactive
3507                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                // The Remove + Add(active=false) above wipes the listener's
3512                // active state. Re-emit an ActivateListener whenever the target
3513                // state keeps it active, otherwise a config change on a still
3514                // active listener would silently deactivate it on replay. This
3515                // subsumes the newly-active (`!my.active && their.active`) case:
3516                // a differing `active` flag always makes the listeners unequal.
3517                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                // any added listener should be unactive
3557                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                // The Remove + Add(active=false) above wipes the listener's
3562                // active state. Re-emit an ActivateListener whenever the target
3563                // state keeps it active, otherwise a config change on a still
3564                // active listener would silently deactivate it on replay. This
3565                // subsumes the newly-active (`!my.active && their.active`) case:
3566                // a differing `active` flag always makes the listeners unequal.
3567                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        // A cluster that gains a health check port while `self` still holds a
3595        // unix backend for it is refused until that backend is replaced
3596        // (`Self::health_check_port_unix_conflict`): its `AddCluster` follows
3597        // the backend orders below.
3598        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        // `base` is `self` once every `RemoveCluster` above has cascaded
3621        // (`Self::remove_cluster`). The frontend and backend diffs start from
3622        // it, not from `self`: they emit no removal the cascade already
3623        // performs, which would otherwise find nothing left to remove, and
3624        // they add back an object `other` still holds for a removed cluster.
3625        // Without a removed cluster, `base` is `self` itself, not a copy.
3626        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        // Certificates have two observable dimensions beyond their map key:
3791        // the payload/expiration stored under a common fingerprint, and the
3792        // insertion order used to break equal-expiration name collisions.
3793        // Build this part of the diff against a certificate-only shadow and
3794        // apply every emitted request to it. That makes combined payload,
3795        // expiry and order changes converge by construction.
3796        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            // `certificate_shadow` may originate from a legacy or externally
3810            // deserialized state. Normalize this address once before the
3811            // loop: every dispatch below then maintains the canonical index
3812            // in O(1) for Add rather than sorting the full address once per
3813            // target certificate.
3814            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                    // Missing target certificates are inserted by the order
3876                    // convergence pass below. Adding them here can put them
3877                    // ahead of retained entries and then needlessly remove
3878                    // and re-add the same fresh certificate.
3879                    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                // Keep the longest prefix of the target that already appears
3915                // as a subsequence of the current order. Moving only the
3916                // remaining suffix to the end, in target order, converges
3917                // this order-only pass without moving the retained prefix.
3918                // For A,B,C -> B,C,A this retains B,C and moves only A. When
3919                // source and target have the same set and at least two
3920                // certificates, one remains globally throughout this pass;
3921                // set changes handled above can still pass through zero. A
3922                // domain carried only by a certificate being moved can also
3923                // briefly fall back or become unavailable. This sequence is
3924                // convergent, not atomic or interruption-free.
3925                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        // Replay symmetry: the request set `diff` emits, when replayed onto a
4001        // clone of `self`, must reproduce `other`'s routing-relevant maps —
4002        // listeners included, with their `active` flag. This is the property the
4003        // hot-reconfig fan-out relies on — a worker applies these requests and
4004        // must converge on `other`. We verify it in debug by actually replaying;
4005        // `dispatch`'s own invariant sweep runs on every step.
4006        //
4007        // Deliberate normalizations. Empty buckets: `backends`/`tcp_fronts` are
4008        // compared after dropping them. `remove_backend`/`remove_tcp_frontend`
4009        // leave an empty `Vec` under a cluster key, whereas `other` may have no
4010        // key at all. An empty bucket emits no requests and is semantically
4011        // equivalent to an absent one, so we normalize it away before comparing.
4012        #[cfg(debug_assertions)]
4013        {
4014            let mut replayed = self.clone();
4015            for request in &v {
4016                // A diff request must always be dispatchable onto `self`.
4017                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            // TCP diff equality is set equality of complete `TcpFrontend` records, so a
4029            // pure permutation of one bucket emits no orders. Project each non-empty
4030            // bucket to a set of full records for this postcondition only; storage and
4031            // request emission are not reordered.
4032            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                    // A legacy `self` has no sidecar and replaying leaves the
4051                    // retained certificates' metadata empty: compare both sides
4052                    // after the same legacy normalization.
4053                    && 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    // FIXME: what about deny rules?
4067    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    /// Gives details about a given cluster.
4106    /// Types like `HttpFrontend` are converted into protobuf ones, like `RequestHttpFrontend`
4107    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    /// Filter the control plane's certificate record.
4200    ///
4201    /// `filters.domain` is matched by **exact SAN equality** and is tested
4202    /// before `filters.fingerprint`, so a request carrying both answers the
4203    /// domain only. That is deliberately NOT the question
4204    /// `sozu certificate list --domain <host>` asks: the certificate that
4205    /// serves a handshake is chosen by `CertificateResolver::domain_lookup`,
4206    /// a `TrieNode` lookup resolving `*.` wildcard and regex labels, so a
4207    /// `*.example.com` certificate is invisible to an exact-equality query
4208    /// for `foo.example.com` (sozu#1383).
4209    ///
4210    /// The trie lives in `sozu-lib`, which depends on this crate — the
4211    /// reverse edge is a cargo cycle — so the operator-facing `--domain`
4212    /// query is resolved one layer up, in `bin`'s
4213    /// `certificates_serving_domain` (`bin/src/command/requests.rs`). Do not
4214    /// reimplement hostname matching here: a second implementation is what
4215    /// drifts from the resolver and reopens sozu#1383.
4216    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        // if no http / https / tcp filter is provided, list all of them
4238        let list_all = !filters.http && !filters.https && !filters.tcp;
4239
4240        // an HTTP/HTTPS frontend with no cluster is a `Deny` rule: it routes
4241        // to no cluster at all, so a cluster filter never matches it.
4242        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        // `FrontendFilters` has no dedicated `udp` flag, so UDP frontends ride
4282        // the same default/all-pass path as TCP: surfaced when no protocol
4283        // filter is set (`list_all`) or when the `tcp` filter is requested.
4284        // Datagram frontends carry no hostname, so a `domain` filter excludes
4285        // them (matching the TCP branch).
4286        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    // create requests needed for a worker to recreate the state
4323    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    /// generate requests necessary to recreate the state,
4334    /// in protobuf, to a temp file
4335    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    /// generate requests necessary to recreate the state,
4350    /// write them in a JSON form in a file, separated by \n\0,
4351    /// returns the number of written requests
4352    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
4381/// One knob's floor, in the shape [`crate::for_each_h2_knob_floor`] expands
4382/// (sozu-proxy/sozu#1418). The list itself lives there, shared with
4383/// `ListenerBuilder::validate_h2_thresholds` in `crate::config`, so this file
4384/// carries the `StateError` wording and not a second copy of the knobs.
4385///
4386/// `u64::from` covers both the `u32` and the `u64` knobs; every field is
4387/// unsigned, so `< 1` is exactly `== 0`.
4388macro_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
4401/// Validate all H2 flood knobs in an HTTP listener patch.
4402///
4403/// Every flood-detector knob (including stream-0 WINDOW_UPDATE) requires a
4404/// value `>= 1`. `0` does not disable the detector — `check_flood` compares
4405/// `count > threshold`, so it makes the first event that counter sees a
4406/// violation, which is every request for `h2_max_header_list_size` and
4407/// `h2_max_header_fields` (also the HPACK decode budget) and the first
4408/// matching frame for the per-window and lifetime counters. The runtime
4409/// constructor `H2FloodConfig::new()` clamps with `.max(1)` instead, but a
4410/// clamp rewrites the operator's stated value, so the control plane refuses
4411/// it here.
4412///
4413/// `h2_max_concurrent_streams` is a connection-config knob that also requires
4414/// `>= 1`; `h2_stream_shrink_ratio` requires `>= 2`.
4415///
4416/// `h2_graceful_shutdown_deadline_seconds = 0` is intentionally **allowed** —
4417/// it means "wait forever (no forced close after GOAWAY)".
4418pub 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
4423/// Validate all H2 flood knobs in an HTTPS listener patch (same rules as HTTP).
4424pub 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
4429/// Validate the H2 knobs of a complete HTTP listener configuration, on the
4430/// `AddHttpListener` path (sozu-proxy/sozu#1418).
4431///
4432/// `UpdateHttpListener` has refused these values since the knobs existed
4433/// ([`validate_h2_flood_knobs_http`]) and `ListenerBuilder::to_http`
4434/// (`command/src/config.rs`) now refuses them at config load — but a raw
4435/// protobuf `AddHttpListener` straight to the command socket went through
4436/// neither. `ConfigState::add_http_listener` clones the config in with no
4437/// validation, `SaveState` re-serialises it, every replay re-injects it, and
4438/// each worker then rewrites the operator's `0` to `1` with the runtime clamp.
4439/// The main process's pre-dispatch validation
4440/// (`bin/src/command/requests.rs::validate_h2_knob_floors`) closes that door.
4441///
4442/// It closes it for what an operator is STATING, not for replay.
4443/// `bin/src/command/requests.rs` runs this only for
4444/// `RequestOrigin::Authored`; on `LoadState` it calls it to `warn!` and count,
4445/// then keeps the listener. An entry the replay gate rejects is skipped, and a
4446/// skipped listener never binds — dropping every frontend behind it to prevent
4447/// one clamped threshold is the worse outcome. The clamp does not tighten the
4448/// knob, it loosens it by one event (`count > threshold` trips on the first
4449/// counted event at `0`, on the second at `1`), but `0` is not "no limit" and
4450/// not a protection level anyone tuned. The worker-side clamp is what serves
4451/// that case.
4452pub 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
4459/// Validate the H2 knobs of a complete HTTPS listener configuration, on the
4460/// `AddHttpsListener` path (same rules as
4461/// [`validate_h2_flood_knobs_http_listener`]).
4462pub 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
4469/// Validate a `sozu_id_header` value against RFC 9110 §5.1 header-name grammar.
4470///
4471/// Rejects empty strings and strings containing CR, LF, colon, space, or tab —
4472/// a conservative approximation of the token grammar that covers all practical
4473/// injection vectors without a full RFC 9110 tokenizer.
4474/// Merge the `answers` map of a listener patch into the listener's stored
4475/// map, as `HttpListener::update_config` and `HttpsListener::update_config`
4476/// (`lib/src/http.rs`, `lib/src/https.rs`) do on the workers: a non-empty
4477/// body replaces the template of its status, an empty body preserves it.
4478fn 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
4486/// Merge a `CustomHttpAnswers` patch into the listener's stored answers,
4487/// preserving any field not present in the patch.
4488///
4489/// Field-mask semantic: `None` in `patch` means "preserve", `Some` means
4490/// "replace". A no-op patch (all-None) leaves `target` untouched. If `target`
4491/// is currently `None`, initialize it from the patch (any `None` field stays
4492/// `None` so hot-upgrade replay sees the same partial state).
4493pub 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
4518/// Validate an `AlpnProtocols` patch: each value must be "h2" or "http/1.1".
4519/// Empty values vec is allowed (reset-to-default).
4520pub 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
4532/// Validate a `sozu_id_header` value against the RFC 9110 §5.1 `token` grammar:
4533///
4534/// ```text
4535/// token  = 1*tchar
4536/// tchar  = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
4537///          "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
4538/// ```
4539///
4540/// Rejects empty strings, non-ASCII bytes, controls (including CR/LF/tab),
4541/// separators (including colon and space), and any other non-`tchar` byte.
4542pub 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
4578/// Validate the `hsts` block of an `UpdateHttpsListenerConfig` patch: it
4579/// replaces the listener default whole, and `enabled` is what tells "disable"
4580/// from "enable", so a block without it is refused — on the main process
4581/// here, on the worker with `ListenerError::HstsEnabledRequired`.
4582pub 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
4592/// Validate a `forwarded_headers` wire value: it must name a
4593/// [`ForwardedHeaders`] variant. prost decodes an unknown enum number into the
4594/// raw `i32`, and the listener getter would silently read it as the default;
4595/// a patch carrying one is refused instead.
4596pub 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
4605/// TCP and UDP frontends are keyed by cluster id: with a cluster filter, look
4606/// the single entry up instead of scanning every cluster; without one, yield
4607/// every frontend.
4608fn 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
4636/// Canonicalize a TCP frontend's ALPN list into its sorted, deduplicated set
4637/// representation. ALPN matching (the worker's `AlpnMatcher::OneOf` in
4638/// `lib/src/tcp.rs`) is semantically a *set* of protocol names, not an
4639/// ordered list: comparing `Vec`s directly treats `["h2", "http/1.1"]` and
4640/// `["http/1.1", "h2"]` as distinct identities, silently admitting a
4641/// duplicate/overlapping frontend the worker rejects on the next fan-out
4642/// (sozu-proxy/sozu#1290). Used both to canonicalize a frontend for storage
4643/// and to canonicalize a candidate for comparison, so `TcpFrontend.alpn` is
4644/// always already in this form.
4645fn canonical_tcp_alpn(alpn: &[String]) -> Vec<String> {
4646    let mut alpn = alpn.to_vec();
4647    alpn.sort();
4648    alpn.dedup();
4649    // POST: strictly increasing (sorted, no adjacent duplicates) — the
4650    // single property every caller relies on to treat two canonical lists
4651    // as set-equal via plain `==`.
4652    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
4659/// Identity predicate for a TCP frontend: `(address, sni, alpn)`, not the
4660/// whole struct -- two frontends differing only in `tags` still collide
4661/// here even though they compare unequal as full structs, since they'd
4662/// still match the exact same wire traffic. `sni` and `alpn` are expected
4663/// to already be in canonical form (normalized-lowercase sni, sorted+deduped
4664/// alpn via [`canonical_tcp_alpn`]) on both sides -- this is a plain
4665/// equality check, not itself a canonicalizer. Shared by
4666/// `add_tcp_frontend`'s duplicate check and `remove_tcp_frontend`'s
4667/// retain/postcondition so the three call sites can never drift apart
4668/// (sozu-proxy/sozu#1290).
4669fn 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
4685//fn diff_map<'a, K:Ord, V: PartialEq>(my: &'a BTreeMap<K,V>, other: &'a BTreeMap<K,V>) -> DiffMap<'a,K,V> {
4686fn 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
4710// this will iterate over the keys of both iterators
4711// since keys are sorted, it should be easy to see which ones are in common or not
4712impl<'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                // there are no more elements in my_it, all the next elements in other
4728                // should be added
4729                // if other was none, we will stop the iterator there
4730                (None, other) => return other.map(|(k, _)| (k, DiffResult::Added)),
4731                // there are no more elements in other_it, all the next elements in my
4732                // should be removed
4733                (Some((k, _)), None) => return Some((k, DiffResult::Removed)),
4734                // element is present in my but not other
4735                (Some((k1, _v1)), Some((k2, v2))) if k1 < k2 => {
4736                    self.other = Some((k2, v2));
4737                    return Some((k1, DiffResult::Removed));
4738                }
4739                // element is present in other byt not in my
4740                (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                    // key is present in both, if elements have changed
4746                    // return a value, otherwise go to the next key for both maps
4747                    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    /// Regression for sozu-proxy/sozu#1404 (#774 surviving in a second
4868    /// implementation of the same verb): `replace_certificate` used to
4869    /// remove the old fingerprint from the address's certificate map
4870    /// BEFORE computing the new fingerprint, and computing it is fallible
4871    /// (`calculate_fingerprint` parses the PEM). A malformed
4872    /// `new_certificate.certificate` therefore made the `?` propagate
4873    /// after the old certificate was already gone, leaving the address
4874    /// with zero certificates. The fix must compute/insert the new
4875    /// certificate before removing the old one, so a failed replace is a
4876    /// true no-op on `self`.
4877    #[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    /// When `ReplaceCertificate` resubmits the SAME PEM (an operator or
4937    /// ACME retry loop), the old and new fingerprints are equal.
4938    /// Mirrors `lib/src/tls.rs`'s
4939    /// `replace_certificate_with_same_fingerprint_is_noop`: an
4940    /// unconditional "insert new, then remove old" must not delete the
4941    /// entry the caller meant to retain merely because old == new.
4942    #[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    /// `AddCertificate` resolves empty name overrides from the certificate's
4992    /// SAN/CN before storing the payload. `ReplaceCertificate` must preserve
4993    /// the same canonical form so SaveState and worker-upgrade replay rebuild
4994    /// exactly the state main accepted.
4995    #[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    /// SaveState, worker bootstrap and worker upgrade all consume
5096    /// `generate_requests`. The request stream must retain both the explicit
5097    /// expiration override and the insertion order that breaks ties between
5098    /// certificates serving the same name.
5099    #[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    /// A same-fingerprint replacement can change names, key material or the
5157    /// explicit expiration without changing the leaf certificate fingerprint.
5158    /// `diff` must emit the replacement instead of treating the common map key
5159    /// as unchanged.
5160    #[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    /// Equal-expiration certificate selection uses insertion order. Two states
5202    /// with identical payload maps but opposite insertion order therefore need
5203    /// a non-empty diff that replays the target order.
5204    #[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(&current).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    /// A 2.3.1 state: HTTPS listener, two clusters with frontends and backends,
5545    /// two certificates with explicit expirations, and no serialized
5546    /// `certificate_metadata` sidecar.
5547    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(&current).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    /// Normalizing a legacy sidecar must not hide explicit differences: a
5706    /// different `expired_at` or insertion order is still a certificate
5707    /// operation.
5708    #[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(&current).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    /// Found while adding certificate coverage to `check_invariants` for
5871    /// sozu-proxy/sozu#1404: `remove_certificate` removed the fingerprint
5872    /// from the address's inner map but never evicted the address key
5873    /// itself, so removing an address's LAST certificate through the
5874    /// ordinary, successful `AddCertificate` + `RemoveCertificate` sequence
5875    /// left a dangling `self.certificates[address] == {}` entry. That
5876    /// leftover is not merely untidy: `generate_requests` (`produce_initial_state`
5877    /// / `SaveState` / worker bootstrap / hot upgrade) skips an empty bucket
5878    /// when emitting `AddCertificate` requests, so replaying those requests
5879    /// into a fresh `ConfigState` never recreates the entry -- it already
5880    /// broke `generate_requests`'s own round-trip debug_assert before this
5881    /// fix. Mirrors the `name_fingerprint_idx` empty-entry leak
5882    /// `CertificateResolver::remove_certificate` (`lib/src/tls.rs`) was
5883    /// fixed for in #1202.
5884    #[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        // Regression proof for the round-trip this leak broke: this is the
5922        // live path SaveState / worker bootstrap / hot upgrade use, and it
5923        // carries its own debug_assert that a fresh replay reproduces `self`.
5924        let _ = state.produce_initial_state();
5925    }
5926
5927    /// Found alongside the `remove_certificate` leak above, same
5928    /// investigation (sozu-proxy/sozu#1404): `add_certificate` computed
5929    /// `add.certificate.fingerprint()` (parses PEM only) and THEN called
5930    /// `self.certificates.entry(address).or_default()` -- unconditionally
5931    /// materialising a bucket for the address, including a brand-new one --
5932    /// BEFORE the second fallible step, `apply_overriding_names`, which
5933    /// additionally parses full X.509 when `names` is empty. Valid PEM
5934    /// armor wrapping invalid X.509 DER content therefore left a dangling
5935    /// empty bucket for a previously-absent address on a REJECTED add,
5936    /// violating the same "a rejected dispatch must be a true no-op"
5937    /// contract `replace_certificate`'s ordering bug violated.
5938    #[test]
5939    fn add_certificate_with_valid_pem_armor_but_invalid_x509_leaves_no_bucket() {
5940        // Valid PEM framing (base64 of arbitrary bytes), but the decoded
5941        // bytes are not a valid X.509 DER structure.
5942        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    /// A backend belongs to its cluster (`ConfigState::add_backend`): add
6313    /// the clusters a test's backends name before them.
6314    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        /*
6414        let encoded = state.encode();
6415        println!("serialized:\n{}", encoded);
6416
6417        let new_state: Option<HttpProxy> = decode_str(&encoded);
6418        println!("deserialized:\n{:?}", new_state);
6419        assert_eq!(new_state, Some(state));
6420        */
6421        //assert!(false);
6422    }
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        // `cluster_2` is gone from `state2`: its `RemoveCluster` also removes
6565        // its frontend and its backend, so the diff emits no separate
6566        // `RemoveHttpFrontend`/`RemoveBackend` for them.
6567        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    /// One cluster per id in `cluster_ids`, each owning one object of every
6616    /// kind a cluster can own: an HTTP, an HTTPS, a TCP and a UDP frontend,
6617    /// and a backend. Every state also carries a deny route
6618    /// (`cluster_id: None`) on `cascade_a`'s hostname, which belongs to no
6619    /// cluster and must outlive the removal of `cascade_a`.
6620    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    /// A route that belongs to no cluster, on the hostname `cascade_a` serves.
6684    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    /// The cluster a routing request names, if any.
6695    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    /// Every routing map two states must agree on.
6718    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    /// Removing a cluster removes, for every protocol, the frontends and the
6740    /// backends that name it, and nothing else: another cluster's objects and
6741    /// a deny route on the removed cluster's own hostname stay.
6742    #[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        // The cascade already removed the frontend: an explicit removal sent
6761        // afterwards has nothing left to remove, and is recognised as such
6762        // (the main process and the workers answer it ok).
6763        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        // A removal naming a cluster that never existed (a typo) is answered
6784        // the same way, and the answer says which route holds the key: here
6785        // the deny route, which the removal leaves in place.
6786        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    /// The HTTP/HTTPS route key (`address;hostname;path;method`) names no
6808    /// cluster, so a route key freed by a cluster removal can be taken by
6809    /// another cluster. A removal still in flight for the first cluster must
6810    /// then leave the second cluster's route alone, whether the first
6811    /// cluster is gone or still there.
6812    #[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            // RemoveCluster(A), then B takes the route key A used, then a
6848            // stale removal of A's frontend arrives.
6849            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            // Same stale removal while A still exists: refused, and not
6882            // mistaken for an object removed with its cluster.
6883            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    /// `diff` towards a state without a cluster emits the `RemoveCluster`
6909    /// alone: the cascade covers the cluster's frontends and backends, and a
6910    /// separate removal of one of them would find nothing left to remove.
6911    #[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    /// A target state may keep a frontend whose cluster it no longer holds.
6935    /// The cascade removes that frontend with its cluster, so `diff` must add
6936    /// it back after the `RemoveCluster`.
6937    #[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    /// SaveState/LoadState and a worker upgrade replay `generate_requests`:
6976    /// once a cluster is removed, none of its frontends or backends may come
6977    /// back through that replay.
6978    #[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    /// A backend belongs to its cluster: an `AddBackend` naming a cluster the
7015    /// state does not hold, never added or already removed, is refused and
7016    /// leaves the state untouched, not even an empty bucket for that cluster.
7017    #[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    /// `AddUnixBackend` obeys the same ownership contract as `AddBackend`, and
7066    /// the guard runs before `unix_path` validation: an unknown cluster is
7067    /// `ClusterNotFound` even for an invalid path, and nothing is stored.
7068    #[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    /// The refusal leaves the error text bounded: a cluster id is protected
7096    /// material (`doc/observability.md`), so `Display` names only its length.
7097    #[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    /// The configuration file emits each cluster's `AddCluster` before its
7115    /// backends, so a TOML load still adds every backend.
7116    #[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    /// `diff` towards a state with a new cluster emits its `AddCluster`
7137    /// before its `AddBackend`s, so the orders replay onto `self`.
7138    #[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    /// A cluster `app` with one backend of fixed identity, either a UNIX
7151    /// backend probed through its socket, or an IP backend probed on `port`.
7152    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    /// `diff` between a cluster without a probe port owning a UNIX backend and
7189    /// the same cluster with a probe port owning the same backend identity as
7190    /// an IP backend (and the reverse) must converge: the port is only
7191    /// accepted once the UNIX backend is gone, and a UNIX backend only once
7192    /// the port is gone.
7193    #[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    /// #152: a backend's `unix_path` survives into the state and back out of
7322    /// the orders it replays to a new worker, so a restart or an upgrade does
7323    /// not turn a unix backend into a TCP one.
7324    #[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    /// #152: the main process refuses a `unix_path` no worker could dial,
7354    /// before it reaches the state.
7355    #[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            // The 8443 HTTPS listener is active in both states but its content
7641            // changed (custom answers). The Remove + Add(active=false) above
7642            // wipes the active flag, so diff must re-emit an ActivateListener to
7643            // keep the listener live across the hot reconfig. Without it the
7644            // worker would silently deactivate the listener on replay.
7645            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        //let diff: HashSet<&RequestContent> = HashSet::from_iter(d.iter());
7656        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        // Round-trip: replaying diff(state -> state2) onto a clone of `state`
7665        // must reproduce `state2`'s listener maps EXACTLY, active flag included.
7666        // This is the hot-reconfig correctness property — a worker applies these
7667        // requests and must converge on the target state. In particular the 8443
7668        // HTTPS listener (active in both states, content changed) must come back
7669        // ACTIVE, which is the bug the ActivateListener re-emission above fixes.
7670        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        // Explicitly assert the still-active listener stays active across the
7689        // config change (the core of the fixed bug).
7690        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 fingerprint: Fingerprint = serde_json::from_str(
7722        //     "\"ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5\"",
7723        // )
7724        // .expect("Could not deserialize the fingerprint");
7725
7726        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        // add backend to a second cluster
7779        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        // remove a backend and verify count decreases
7793        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    /// The order a worker replays frontends in is the `http_fronts`
7807    /// `BTreeMap` key order, and that key begins with the address and the
7808    /// hostname (`RequestHttpFrontend`'s `Display`, `command/src/request.rs`).
7809    /// `/test[0-9]/.example.com` therefore sorts BEFORE `test4.example.com`
7810    /// — `/` is 0x2F, `t` is 0x74 — whatever order the operator added the
7811    /// two frontends in. `produce_initial_state` and the protobuf
7812    /// `InitialState.requests` it fills both preserve that order, and the
7813    /// worker side walks it in sequence, so the regex segment reaches
7814    /// `add_tree_rule` first on every launch of every worker.
7815    ///
7816    /// This is what sozu#1351's no-migration claim rests on. Two
7817    /// consequences, and the test pins the fact both follow from:
7818    ///
7819    /// - On a build before the fix the divergence was PERMANENT, not
7820    ///   transient: the leaking order is the only order a forked worker
7821    ///   ever sees, so a restart reproduced it instead of healing it. A
7822    ///   correct table built live by an exact-first sequence of API calls
7823    ///   is therefore re-corrupted at the next worker fork or upgrade — the
7824    ///   live order is never the durable one.
7825    /// - On the fixed build the same replay yields the corrected routing
7826    ///   with no state rewrite, because both orders now build the same
7827    ///   table (`lib/src/router/mod.rs`'s
7828    ///   `hostname_routing_does_not_depend_on_declaration_order`).
7829    ///
7830    /// To SEE THIS RED: in `ConfigState::generate_requests`, collect the
7831    /// `http_fronts` values into a `Vec`, reverse it, and push from that —
7832    /// or declare `http_fronts` as a `HashMap`. The asserted order stops
7833    /// holding. Adding the two frontends in the other order below does NOT
7834    /// redden it, which is the point.
7835    #[test]
7836    fn a_regex_hostname_is_always_replayed_before_the_exact_name_it_matches() {
7837        let mut state: ConfigState = Default::default();
7838
7839        // Added exact-first: the order that routed correctly in-process on
7840        // an affected build, and the one the replay does not preserve.
7841        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        // Asserted through `produce_initial_state`, not `generate_requests`
7858        // alone, so the envelope the worker actually reads is covered too.
7859        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        // add an HTTP frontend
7887        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        // add an HTTPS frontend
7903        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        // add a TCP frontend
7919        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        // add a second TCP frontend on the same cluster
7932        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        // remove the HTTP frontend
7945        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    /// A state that holds an HTTPS listener requiring a client certificate
7962    /// must rebuild it on `AddHttpsListenerWithClientAuth`: those requests
7963    /// are the initial state of every new worker, the saved state file and
7964    /// the reload diff, and a peer that predates mutual TLS would build the
7965    /// listener without client auth from `AddHttpsListener`.
7966    #[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    /// A configuration reload must be able to refresh the CA bundle or the
8019    /// CRLs of a running listener: replaying `AddHttpsListener` is refused
8020    /// for an address the state holds, so the reload sends the patch
8021    /// `client_auth_refresh` builds, and the state applies it.
8022    #[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    // ── helpers ────────────────────────────────────────────────────────────────
8091
8092    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    /// Runtime refusal must restore an activation only when the request would
8140    /// have changed the stored flag. This keeps the inverse scoped to the
8141    /// exact `(protocol, address, interface)` listener and prevents a repeated
8142    /// activation failure from deactivating an already-live listener.
8143    #[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    /// Mandatory roundtrip guard for the UDP control-plane data model.
8213    ///
8214    /// 1. Build a state holding an active UDP listener + UDP frontend, run
8215    ///    `generate_requests()`, replay every emitted request into a fresh
8216    ///    `ConfigState`, and assert the two states are byte-for-byte equal.
8217    /// 2. Prove a `diff()` between an empty state and the UDP-bearing state
8218    ///    produces requests that, replayed, reconstruct the UDP listener and
8219    ///    frontend (a hot-add path), and that the reverse diff tears them
8220    ///    down again.
8221    #[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        // `request_counts` is a runtime census side-effect of `dispatch`; it
8259        // diverges by construction whenever the number/shape of replayed
8260        // requests differs from the originals, so compare the logical config
8261        // with the census cleared on both sides.
8262        let logical = |s: &ConfigState| {
8263            let mut c = s.clone();
8264            c.request_counts.clear();
8265            c
8266        };
8267
8268        // 1. generate_requests → replay → equal
8269        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        // 2. diff from empty reconstructs the UDP objects
8282        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        // reverse diff tears them back down to empty
8296        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        // update path: a partial patch mutates the stored listener in place
8316        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    /// Mandatory roundtrip + identity guard for SNI/ALPN-scoped TCP
8337    /// frontends (sozu-proxy/sozu#1279).
8338    ///
8339    /// 1. Build a state holding a TCP listener with non-default SNI-preread
8340    ///    knobs plus two SNI-scoped frontends on the same address, run
8341    ///    `generate_requests()`, replay every emitted request into a fresh
8342    ///    `ConfigState`, and assert the two states are byte-for-byte equal.
8343    /// 2. Prove `diff()` between an empty state and this state reconstructs
8344    ///    the listener + both frontends (hot-add), and the reverse diff
8345    ///    tears them back down.
8346    /// 3. Prove add/remove identity is `(address, sni, alpn)`: removing one
8347    ///    SNI-scoped sibling leaves the other untouched, and a
8348    ///    address-only removal (mismatched sni/alpn) is rejected rather
8349    ///    than silently nuking every frontend at that address.
8350    #[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        // `request_counts` is a runtime census side-effect of `dispatch`; it
8416        // diverges by construction whenever the number/shape of replayed
8417        // requests differs from the originals, so compare the logical config
8418        // with the census cleared on both sides.
8419        let logical = |s: &ConfigState| {
8420            let mut c = s.clone();
8421            c.request_counts.clear();
8422            c
8423        };
8424
8425        // 1. generate_requests → replay → equal
8426        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        // 2. diff from empty reconstructs the TCP objects
8439        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        // reverse diff tears them back down to empty
8453        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        // 3a. an address-only removal (mismatched sni/alpn) must not match
8470        // either sibling — identity is (address, sni, alpn), not address
8471        // alone.
8472        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        // 3b. removing the exact (address, sni, alpn) identity of frontend_a
8493        // must drop only that entry and leave frontend_b untouched.
8494        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    /// A TCP frontend request whose `route` tag identifies it in order checks.
8509    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    /// Four admissible frontends of ONE cluster, already in canonical form
8526    /// (lowercase sni, sorted alpn), tagged `A`..`D`.
8527    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    /// A state built by dispatching `AddTcpFrontend` in the given order, so the
8561    /// stored per-cluster `Vec` order is exactly the dispatch order.
8562    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    /// The `route` tags of one cluster's stored TCP bucket, in stored order.
8573    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    /// Per-cluster sets of complete TCP frontend records, empty buckets dropped.
8587    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    /// `from.diff(to)`, asserting neither input state was modified.
8597    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    /// A pure permutation of complete TCP frontends within one cluster is not
8621    /// a semantic change: no orders, and the debug replay postcondition holds.
8622    #[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    /// `[A] -> [C, A]` is exactly the semantic addition of `C`.
8653    #[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    /// Rebuilding several clusters' TCP frontends from an empty state replays
8681    /// to the same per-cluster sets of complete records, with no empty bucket
8682    /// introduced; tearing them down normalizes away the emptied buckets.
8683    #[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    /// `state.diff(&state)` is empty and, like every diff call, leaves the
8743    /// exact stored per-cluster order untouched.
8744    #[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    /// Saved/self-generated request order stays faithful: replaying
8777    /// `generate_requests` into an empty state reproduces a deliberately
8778    /// non-sorted single-cluster TCP bucket exactly (full `Vec` equality).
8779    #[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        // `request_counts` is a runtime census side-effect of `dispatch`;
8801        // clear only it, as the other strict roundtrip helpers do.
8802        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    // ── master-side TCP frontend admission parity (sozu-proxy/sozu#1290) ──────
8823    //
8824    // The command server mutates master `ConfigState` before fanning a
8825    // request out to workers, and a worker NACK never rolls the master back.
8826    // These tests prove the master rejects everything the worker's
8827    // `validate_new_tcp_front` (`lib/src/tcp.rs`) would reject, BEFORE any
8828    // mutation -- every rejection case asserts `hash_state()` and
8829    // `count_tcp_frontends_raw()` (plus the relevant bucket lengths) are
8830    // byte-for-byte unchanged, not just that the call returned `Err`.
8831
8832    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    /// The (address, sni, alpn) identity check must span ALL clusters at a
8847    /// listener address, not just the candidate's own `cluster_id` bucket --
8848    /// otherwise the same identity under a different cluster_id is silently
8849    /// accepted by the master and then rejected by every worker.
8850    #[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    /// ALPN identity is a *set*: the same protocols in a different order
8906    /// must still collide as a duplicate, even within one cluster's bucket.
8907    #[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    /// Symmetric counterpart: removal must also treat ALPN as a set, so a
8953    /// request differing only in protocol order still matches the stored
8954    /// (canonical) frontend instead of falling through to `NoChange`.
8955    #[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    /// ALPN-set overlap on the same (address, normalized sni) must be
8992    /// rejected even across different clusters -- the previous check never
8993    /// looked outside the candidate's own cluster bucket at all.
8994    #[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    /// Listener-wide no-SNI/SNI mixing must be enforced in both directions,
9045    /// across cluster buckets.
9046    #[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        // Direction 1: a no-SNI frontend exists; adding an SNI-scoped
9057        // frontend at the SAME address must be rejected.
9058        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        // Direction 2: an SNI-scoped frontend exists; adding a no-SNI
9091        // frontend at the SAME address must be rejected.
9092        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    /// A malformed SNI pattern must be rejected by the master itself, not
9126    /// merely by config-load's `to_tcp_front` -- a raw `AddTcpFrontend` over
9127    /// the command socket, or a `LoadState` replay, bypasses `config.rs`
9128    /// entirely.
9129    #[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                // Leading empty label -- rejected by `validate_sni_pattern`.
9144                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    /// A non-empty `alpn` without `sni` must be rejected by the master:
9167    /// alpn only matches within an SNI-scoped preread, so a no-SNI frontend
9168    /// would silently ignore its alpn list. Config-load (`to_tcp_front`) and
9169    /// the worker both reject this shape; a raw `AddTcpFrontend` over the
9170    /// command socket must not slip past the master.
9171    #[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    /// Worker-parity guard: an exact catch-all and a wildcard catch-all on
9215    /// the SAME address are DISTINCT (address, sni) keys and must both be
9216    /// accepted -- the listener-wide admission checks above must not
9217    /// over-forbid this legal combination by conflating exact and wildcard
9218    /// SNI as the same key.
9219    #[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    /// `list_frontends` must surface UDP frontends alongside TCP ones. The
9258    /// proto `FrontendFilters` has no `udp` flag, so UDP rides the default
9259    /// (all-pass) and `tcp` filters; a `domain` filter excludes both.
9260    #[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        // default filters (all false, no domain) → list everything, incl. UDP
9288        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        // explicit `tcp` filter surfaces both TCP and UDP (no `udp` flag exists)
9299        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        // an `http` filter excludes both TCP and UDP
9311        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        // a `domain` filter excludes UDP (no hostname), matching TCP behaviour
9319        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    /// `list_frontends` must honour the `cluster_id` filter on every protocol,
9328    /// combine it with the `domain` filter, and never match a `Deny` frontend
9329    /// (one with no cluster id at all).
9330    #[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            // a `Deny` frontend: routes to no cluster
9339            (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        // TCP/UDP frontends are keyed by address, so each cluster gets its own
9369        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        // no filter → everything, including the `Deny` frontends
9393        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        // cluster filter alone → one frontend of each protocol, and only the
9400        // wanted one (positive AND negative space)
9401        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        // an unknown cluster id matches nothing at all
9425        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        // `cluster_id` and `domain` are ANDed, not ORed
9435        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        // the cluster filter composes with a protocol filter
9444        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    // ── update_https_listener ──────────────────────────────────────────────────
9456
9457    /// Happy path: patching two H2 flood knobs updates the map entry; all other
9458    /// fields are left untouched.
9459    #[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        // Untouched fields must be unchanged
9484        assert_eq!(listener.front_timeout, 60);
9485        assert_eq!(listener.h2_max_settings_per_window, None);
9486    }
9487
9488    /// NotFound: patching a listener address that was never registered returns
9489    /// `StateError::NotFound`.
9490    #[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    /// No-op: a patch with only `address` set (all options None) is Ok and does
9514    /// not change any field.
9515    #[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    /// InvalidValue: setting a flood knob to 0 must be rejected.
9541    #[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    /// AddCluster: an inline `cluster.health_check` with a CRLF-bearing URI
9570    /// must be rejected before the cluster lands in `ConfigState`. Without
9571    /// this guard, TOML reload / SaveState / direct API AddCluster requests
9572    /// bypass the SetHealthCheck-side check and let an attacker-controlled
9573    /// health-check URI smuggle CR/LF into outbound HTTP/1.1 probes.
9574    #[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        // A keyed cluster refuses a TCP frontend.
9596        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        // A cluster serving a TCP frontend refuses a key.
9610        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    /// A cluster budget allowing no attempt, or more than the attempt
9640    /// counters hold, is refused; an accepted one is stored with the cluster,
9641    /// so it is saved and replayed with it (sozu-proxy/sozu#1800).
9642    #[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    /// #1801: `SetHealthCheck` refuses a TCP-mode probe carrying a status
9802    /// field, and keeps the previous config; the same probe without one is
9803    /// stored.
9804    #[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    /// `SetHealthCheck` refuses a probe port outside `1..=65535` and keeps
9860    /// the previous config; a valid port is stored as sent.
9861    #[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    /// Custom health-check headers: validation runs before any mutation, and
9920    /// the exact ordered values survive query, diff, the saved-state file and
9921    /// a generated replay.
9922    #[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        // Metadata only: the fingerprint stands for a value the state never holds.
9934        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        // A reserved name: refused by validation, not by the type.
9961        let invalid = probe(vec![header("Host", "1")]);
9962
9963        // AddCluster with an invalid inline health check leaves no cluster.
9964        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        // A refused SetHealthCheck keeps the previous list, byte for byte.
10001        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        // `request_counts` is a census of dispatched orders, not configuration.
10014        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        // Query: cluster state and the health-check listing.
10030        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        // Diff from an empty state, and from a state with other headers.
10041        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        // Saved-state file: write, parse back, replay.
10061        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        // Generated replay.
10081        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    /// A protected order reaches the state as metadata: names and the main
10096    /// process's fingerprints only, validated before any mutation. An order
10097    /// with a header lacking its fingerprint, or whose carriers do not follow
10098    /// its headers, is refused and the previous configuration stays.
10099    #[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    /// A probe `port` targets a backend IP, a `unix_path` backend has none:
10168    /// whichever arrives second is refused, and the cluster, its health check
10169    /// and its backends stay as they were.
10170    #[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        // The probe port is there first.
10223        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        // The unix backend is there first, through `SetHealthCheck`, then an
10232        // `AddCluster` upsert.
10233        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        // An unknown cluster keeps its own diagnostic.
10247        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    /// ALPN validation: reject unknown ALPN values.
10259    #[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    /// ALPN validation: empty values vec = reset to default, must be accepted.
10292    #[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    /// ALPN validation: valid values ["h2", "http/1.1"] must be accepted.
10321    #[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    /// ALPN absent wrapper: when `alpn_protocols` is None in the patch, the
10347    /// listener's ALPN field must not be touched.
10348    #[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        // patch with no alpn_protocols field
10359        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    /// sozu_id_header validation: empty string must be rejected.
10377    #[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    /// sozu_id_header validation: value containing colon must be rejected.
10406    #[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    /// sozu_id_header validation: well-formed token must be accepted.
10435    #[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    /// sozu#322: an `AddHttpListener` / `AddHttpsListener` carrying a
10457    /// `forwarded_headers` wire value that names no `ForwardedHeaders`
10458    /// variant is refused and stores nothing, as an update carrying one is.
10459    /// Without this the state would record the raw number while the worker
10460    /// reads it as the default. A known value is accepted.
10461    #[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    /// sozu#322: `forwarded_headers` is hot-updatable on HTTP and HTTPS
10520    /// listeners — the main-process state records the patched mode, so a
10521    /// `ListListeners`, a `SaveState` and an upgrade replay all carry it —
10522    /// and a wire value naming no `ForwardedHeaders` variant is refused.
10523    #[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        // A refused patch leaves the recorded mode as it was.
10597        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    /// h2_graceful_shutdown_deadline_seconds = 0 must be allowed (means "wait forever").
10608    #[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    // ── update_http_listener ───────────────────────────────────────────────────
10630
10631    /// Happy path for HTTP listener patch.
10632    #[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        // untouched
10654        assert_eq!(listener.back_timeout, 30);
10655    }
10656
10657    /// sozu-proxy/sozu#1715: the `answers` map of an HTTP listener patch is
10658    /// recorded in the main state as the workers apply it
10659    /// (`HttpListener::update_config`, `lib/src/http.rs`): a non-empty body
10660    /// replaces the stored template for its status, an empty body preserves
10661    /// it, and the other statuses are untouched. Without it `SaveState`,
10662    /// `ListListeners` and the replay to an upgraded worker lose the patch.
10663    #[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    /// sozu-proxy/sozu#1715: an HTTPS listener patch records its `answers`
10709    /// map and its `hsts` block in the main state as the workers apply them
10710    /// (`HttpsListener::update_config`, `lib/src/https.rs`). `hsts` is a
10711    /// full-object replacement, and a patch without it keeps the recorded
10712    /// policy.
10713    #[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    /// HTTP listener: flood knob 0 is rejected.
10781    #[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    // ── X-Real-IP knobs (sozu#1688) ─────────────────────────────────────────────
10810
10811    /// A hot patch of `elide_x_real_ip` / `send_x_real_ip` on an HTTP listener
10812    /// is recorded in the main state, which `listener list` and `SaveState`
10813    /// read.
10814    #[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        // untouched
10836        assert_eq!(listener.front_timeout, 60);
10837    }
10838
10839    /// A hot patch of `h2c_prior_knowledge` on an HTTP listener is recorded in
10840    /// the main state, and a patch that does not name it preserves it
10841    /// (sozu-proxy/sozu#1911).
10842    #[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        // untouched
10869        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    /// An absent X-Real-IP field preserves the recorded value, and patching one
10888    /// knob never writes the other.
10889    #[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    /// A hot patch of the X-Real-IP knobs on an HTTPS listener is recorded in
10917    /// the main state.
10918    #[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        // untouched
10940        assert_eq!(listener.front_timeout, 60);
10941    }
10942
10943    /// An absent X-Real-IP field preserves the recorded HTTPS value, and
10944    /// patching one knob never writes the other.
10945    #[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    /// The hot X-Real-IP values survive `generate_requests`, the replay a new
10973    /// worker receives on upgrade or restart: replaying it must not revert them.
10974    #[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    // ── update_tcp_listener ────────────────────────────────────────────────────
11030
11031    /// Happy path for TCP listener patch.
11032    #[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); // untouched
11052    }
11053
11054    /// TCP listener: NotFound when address is unknown.
11055    #[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    // ── rejected listener patches are all-or-nothing (sozu#1703) ───────────────
11079
11080    /// The state a rejected request must leave intact: everything but
11081    /// `request_counts`, which `ConfigState::dispatch` bumps before it runs
11082    /// the verb, accepted or not.
11083    fn without_request_counts(state: &ConfigState) -> ConfigState {
11084        ConfigState {
11085            request_counts: BTreeMap::new(),
11086            ..state.clone()
11087        }
11088    }
11089
11090    /// A rejected `UpdateHttpListener` leaves the main state exactly as it was.
11091    /// Each patch carries fields that validate and are applied early, then one
11092    /// invalid field: the old code wrote the early fields, validated the late
11093    /// one, and returned the error with the early writes kept, so
11094    /// `listener list`, `SaveState` and upgrade replay showed a patch that was
11095    /// refused.
11096    ///
11097    /// To SEE THIS RED: in `ConfigState::update_http_listener`, move the
11098    /// `validate_sozu_id_header` / `validate_forwarded_headers` calls back
11099    /// into the field writes they guard.
11100    #[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        // The same prefix alone is accepted: the rejections above come from
11152        // the invalid field, not from the prefix.
11153        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    /// The HTTPS counterpart, with the two HTTPS-only late failures: an unknown
11162    /// ALPN value (validated mid-way through the old write sequence) and an
11163    /// `hsts` block without `enabled`, which every worker refuses
11164    /// (`ListenerError::HstsEnabledRequired`) but the main state used to
11165    /// accept, so the main process and its workers disagreed on the verdict.
11166    ///
11167    /// To SEE THIS RED: in `ConfigState::update_https_listener`, move the
11168    /// field validators back into the field writes they guard, or drop the
11169    /// `hsts` check.
11170    #[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    /// `update_tcp_listener` validates nothing past the listener lookup, so a
11253    /// TCP patch can only be refused before its first write. This pins that:
11254    /// a refused TCP patch leaves the main state unchanged.
11255    #[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    /// `ConfigState::dispatch` MUST treat `SetMetricDetail` as a
11282    /// runtime-only verb (no persisted state mutation). A future
11283    /// refactor that drops the variant from the no-op match arm and
11284    /// falls through to the catch-all would silently re-break the
11285    /// SetMetricDetail dispatch path with `UndispatchableRequest`.
11286    #[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    /// `remove_udp_frontend` removes ONLY the frontend the caller named: its
11306    /// key mirrors `add_udp_frontend`'s, exactly as the TCP pair does.
11307    ///
11308    /// [`ConfigState::add_udp_frontend`] stores a full
11309    /// `UdpFrontend { cluster_id, address, tags }` and admits one frontend per
11310    /// address across every cluster, so the entries a bucket holds sit at
11311    /// DIFFERENT addresses — this test asserts that premise first so it cannot
11312    /// rot. [`ConfigState::remove_udp_frontend`] retains on that same
11313    /// (cluster, address, tags) identity — the bucket already scopes
11314    /// `cluster_id` — so a removal drops exactly one entry and leaves every
11315    /// other frontend in the bucket alone. This is the mirror
11316    /// `remove_tcp_frontend` has always had for its own (address, sni, alpn)
11317    /// key, and it is what makes `RemoveUdpFrontend` a true inverse of
11318    /// `AddUdpFrontend` for `compute_rollback` (`bin/src/command/requests.rs`,
11319    /// `a_udp_frontend_add_inverts_to_its_exact_removal`).
11320    ///
11321    /// The tags are part of the key in BOTH directions, so a remove carrying
11322    /// no tags does not clear a tagged frontend — the observable semantics
11323    /// this test also pins. `sozu frontend udp remove --tags` carries them,
11324    /// mirroring `--sni` / `--alpn` on the TCP remove.
11325    ///
11326    /// To SEE THIS RED, one mutation per half:
11327    ///
11328    /// - The SIBLING half: widen `matches_removal` in
11329    ///   [`ConfigState::remove_udp_frontend`] to the whole bucket,
11330    ///   `|_front: &UdpFrontend| true`. In a build with `debug_assertions` the
11331    ///   production guard fires first — `remove_udp_frontend drops exactly one
11332    ///   entry, left: 0, right: 1` — which is the TCP-twin assertion doing its
11333    ///   job. Strip that `debug_assert_eq!` and its companion too and the
11334    ///   failure lands on the `surviving == ["team-b"]` assertion instead.
11335    /// - The TAGLESS half, which the mutation above never reaches: restore the
11336    ///   pre-fix address-only retain by reducing `matches_removal` to
11337    ///   `|front: &UdpFrontend| front.address == remove_address`. The tagged
11338    ///   removal still drops exactly one entry, so the sibling half and both
11339    ///   production assertions pass; the ONLY failure is the
11340    ///   `expect_err("a tagless remove must not match a tagged UDP frontend")`
11341    ///   below. That half is what existing operator scripts hit, so it is
11342    ///   pinned on its own rather than shadowed by the sibling assertion.
11343    #[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        // The premise: one cluster, two addresses — one frontend per address
11355        // is what `add_udp_frontend` admits and what the worker's
11356        // one-cluster-per-listener datapath can represent.
11357        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        // The other half of the narrowed key: tags belong to the removal
11389        // identity in BOTH directions, so a remove carrying none matches
11390        // nothing at that address instead of clearing the frontend on it.
11391        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    /// A UDP listener address is the ENTIRE routing key — no SNI, no `Host`
11419    /// header, no path — so [`ConfigState::add_udp_frontend`] admits at most
11420    /// one frontend per address, across every cluster.
11421    ///
11422    /// Both shapes refused here were admitted before and neither could be
11423    /// represented by the worker: `UdpProxy::add_udp_front` (`lib/src/udp.rs`)
11424    /// overwrites `listener.cluster_id` and the listener's tags, and
11425    /// `cluster_for_listener: HashMap<Token, ClusterId>` maps one listener
11426    /// token to exactly one cluster. The second of a same-address pair simply
11427    /// replaced the first on the datapath.
11428    ///
11429    /// - The SAME-CLUSTER half: two frontends at one (cluster, address)
11430    ///   differing only in their access-log tags. The old dedup compared the
11431    ///   FULL `UdpFrontend { cluster_id, address, tags }`, so a different tag
11432    ///   set walked straight past it.
11433    /// - The CROSS-CLUSTER half: a second cluster claiming the address. The
11434    ///   old scan never left `front.cluster_id`'s own bucket, so this met no
11435    ///   check whatsoever.
11436    ///
11437    /// To SEE THESE RED (regression proof): restore the pre-fix body of
11438    /// [`ConfigState::add_udp_frontend`] — open the bucket first with
11439    /// `let udp_frontends = self.udp_fronts.entry(front.cluster_id.clone()).or_default();`
11440    /// and replace the listener-wide `find` and both its `Err` returns with
11441    /// the bucket-local
11442    /// `if udp_frontends.contains(&udp_frontend) { return Err(StateError::Exists { .. }); }`.
11443    /// Both `expect_err`s below then fail with
11444    /// `a second UDP frontend on a taken address must be refused: ()`.
11445    /// The DIFFERENT-address add and the add → remove → add sequence stay
11446    /// green under that mutation — they are here to pin that the check refuses
11447    /// only a genuine collision and leaks no state once the address is freed.
11448    #[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        // SAME cluster, same address, different tags.
11459        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        // DIFFERENT cluster, same address — the case with no check at all
11464        // before: it landed in another bucket.
11465        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        // A different address is a different listener: still admitted.
11480        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        // add -> remove -> add on the same address: the check must read live
11487        // state, never a residue of the frontend that has been removed.
11488        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    /// The refusal names the frontend already holding the address, so an
11503    /// operator can remove it without dumping the whole state.
11504    ///
11505    /// `doc/observability.md`'s sensitive-value logging boundary lists cluster
11506    /// identifiers and access-log tags as protected material in error text, so
11507    /// the identifiers travel in the variant's typed fields — lossless for a
11508    /// caller that matches it — while `Display`/`Debug` render the address and
11509    /// bounded metadata only. That is the same split `StateError::Exists` and
11510    /// `StateError::InvalidTcpFrontend` already make.
11511    ///
11512    /// To SEE THIS RED: make the cross-cluster arm of
11513    /// [`ConfigState::add_udp_frontend`] answer the generic
11514    /// `StateError::Exists { kind: ObjectKind::UdpFrontend, id: format!("{udp_frontend:?}") }`
11515    /// instead of `StateError::UdpFrontendAddressTaken`. The `match` below
11516    /// fails with `expected UdpFrontendAddressTaken, got StateError(...)`, and
11517    /// with it the operator's only pointer to the current owner.
11518    #[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        // The logging projection stays inside the sensitive-value boundary:
11567        // the address is lossless, the identifiers are metadata only.
11568        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    /// A configuration holding one UDP frontend per address is what the new
11592    /// admission rule accepts, and `SaveState` -> `LoadState` must carry it
11593    /// through byte-for-byte: every entry the file holds is dispatched again
11594    /// without a single refusal.
11595    ///
11596    /// This drives the real file format — `write_requests_to_file`'s
11597    /// `\n\0`-separated JSON `WorkerRequest` records, read back through
11598    /// `parse_several_requests`, the exact pair
11599    /// `bin/src/command/requests.rs::{save_state, load_state}` uses — rather
11600    /// than an in-memory `generate_requests` replay.
11601    ///
11602    /// To SEE THIS RED: key the listener-wide scan in
11603    /// [`ConfigState::add_udp_frontend`] on the PORT alone,
11604    /// `existing.address.port() == address.port()`, the plausible over-broad
11605    /// reading of "one frontend per listener". The two frontends below share
11606    /// port 5353 on different IPs, so the over-broad check refuses the second
11607    /// while the state is being built and the test fails on
11608    /// `one UDP frontend per address must be admitted: StateError(UDP frontend
11609    /// address 127.0.0.2:5353 is already claimed by another frontend:
11610    /// cluster_id_bytes=6 current_cluster_id_bytes=3 current_tag_count=1)` --
11611    /// before it ever reaches the replay assertion, which is the point: an
11612    /// address-keyed check must let two listeners share a port.
11613    #[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    // ── listener identity: (address, interface) ─────────────────────────────
11692
11693    fn with_interface<T>(mut listener: T, set: impl FnOnce(&mut T)) -> T {
11694        set(&mut listener);
11695        listener
11696    }
11697
11698    /// Two listeners on the same address bound to different interfaces are two
11699    /// distinct listeners: both are recorded, keyed `address%interface`, next
11700    /// to the one on the same address with no interface, for every protocol.
11701    #[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        // The same (address, interface) twice is still a duplicate.
11771        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        // Activate, deactivate and remove target exactly one listener.
11784        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        // Removing an interface that was never added is an error, and does not
11817        // fall back to the listener without an interface.
11818        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        // A SaveState -> LoadState round trip rebuilds every listener with its
11832        // interface, and the diff between the two states is empty.
11833        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    /// The diff between two states tells listeners apart by interface: moving a
11844    /// listener from one interface to another removes one and adds the other,
11845    /// and the remove request names the interface it removes.
11846    #[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    /// The reload path: adding a listener on an interface next to one that
11883    /// already routes frontends yields only that listener's add and
11884    /// activation. The frontends are not sent again — they already sit on the
11885    /// address — so the worker must give the new listener its sibling's routes
11886    /// (`HttpProxy::add_listener`, `lib/src/http.rs`).
11887    #[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    /// An update patch names its listener by address only (the interface is
11924    /// not patchable). With two listeners on that address it cannot tell them
11925    /// apart, so it is refused rather than applied to an arbitrary one.
11926    #[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        // One listener on the address: the patch applies to it, interface or not.
11940        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    /// sozu-proxy/sozu#1253: header predicates are part of a frontend's
11981    /// state key. A canary that differs from its base only by a predicate
11982    /// is stored beside it, a respelling of the same conjunction (other
11983    /// order, other name case) is the same key, removing the canary leaves
11984    /// the base, and a frontend without predicates keeps its historical key.
11985    #[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        // The worker deduplicates repeated predicates, so a repeated
12025        // predicate (even under another name case) shares the single key.
12026        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        // A replay of the saved orders rebuilds the same state, predicates
12051        // included.
12052        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        // The replay (and any worker bound to it) sees the predicated
12059        // frontend on its dedicated verb only, so an older worker refuses it
12060        // instead of dropping the guard; the base keeps the historical one.
12061        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        // Rolling back a predicated addition removes that route only, on the
12075        // matching verb, in either spelling of the add.
12076        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    /// `CloseSessions` acts on live sessions only: the state accepts it,
12108    /// stores nothing, and neither `generate_requests` (what `save_state`
12109    /// writes and `load_state` / an upgrade replay) nor the cluster hashes
12110    /// change.
12111    #[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}