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