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