Skip to main content

sozu_command_lib/
state.rs

1use std::{
2    collections::{
3        BTreeMap, BTreeSet, HashMap, HashSet, btree_map::Entry as BTreeMapEntry,
4        hash_map::DefaultHasher,
5    },
6    fs::File,
7    hash::{Hash, Hasher},
8    io::Write,
9    iter::repeat,
10    net::SocketAddr,
11};
12
13use prost::{Message, UnknownEnumValue};
14
15use crate::{
16    ObjectKind,
17    certificate::{CertificateError, Fingerprint, calculate_fingerprint},
18    config::validate_sni_pattern,
19    proto::{
20        command::{
21            ActivateListener, AddBackend, AddCertificate, CertificateAndKey, Cluster,
22            ClusterInformation, CustomHttpAnswers, DeactivateListener, FrontendFilters,
23            HealthChecksList, HttpListenerConfig, HttpsListenerConfig, InitialState,
24            ListedFrontends, ListenerType, ListenersList, PathRule, QueryCertificatesFilters,
25            RemoveBackend, RemoveCertificate, RemoveListener, ReplaceCertificate, Request,
26            RequestCounts, RequestHttpFrontend, RequestTcpFrontend, RequestUdpFrontend,
27            SetHealthCheck, SocketAddress, TcpListenerConfig, UdpListenerConfig,
28            UpdateHttpListenerConfig, UpdateHttpsListenerConfig, UpdateTcpListenerConfig,
29            UpdateUdpListenerConfig, WorkerRequest, request::RequestType,
30        },
31        display::format_request_type,
32    },
33    response::{Backend, HttpFrontend, TcpFrontend, UdpFrontend},
34};
35
36/// To use throughout Sōzu
37pub type ClusterId = String;
38
39#[derive(thiserror::Error, Debug)]
40pub enum StateError {
41    #[error("Request came in empty")]
42    EmptyRequest,
43    #[error("dispatching this request did not bring any change to the state")]
44    NoChange,
45    #[error("State can not handle this request")]
46    UndispatchableRequest,
47    #[error("Did not find {kind:?} with address or id '{id}'")]
48    NotFound { kind: ObjectKind, id: String },
49    #[error("{kind:?} '{id}' already exists")]
50    Exists { kind: ObjectKind, id: String },
51    #[error("Wrong field value: {0}")]
52    WrongFieldValue(UnknownEnumValue),
53    #[error("Could not add certificate: {0}")]
54    AddCertificate(CertificateError),
55    #[error("Could not remove certificate: {0}")]
56    RemoveCertificate(String),
57    #[error("Could not replace certificate: {0}")]
58    ReplaceCertificate(String),
59    #[error(
60        "Could not convert the frontend to an insertable one. Frontend: {frontend} error: {error}"
61    )]
62    FrontendConversion { frontend: String, error: String },
63    #[error("Could not write state to file: {0}")]
64    FileError(std::io::Error),
65    #[error("Invalid value for field '{field}': {reason}")]
66    InvalidValue {
67        field: &'static str,
68        reason: &'static str,
69    },
70    /// Mirrors the worker's `ProxyError::InvalidTcpFrontend`
71    /// (`lib/src/tcp.rs`): the master rejects an `AddTcpFrontend` for the
72    /// same reasons `validate_new_tcp_front` would, so a route never lands
73    /// in `ConfigState` only to be NACKed by every worker on the next
74    /// fan-out (sozu-proxy/sozu#1290).
75    #[error("invalid TCP frontend for address {address}: {reason}")]
76    InvalidTcpFrontend { address: SocketAddr, reason: String },
77}
78
79/// The `ConfigState` represents the state of Sōzu's business, which is to forward traffic
80/// from frontends to backends. Hence, it contains all details about:
81///
82/// - listeners (socket addresses, for TCP and HTTP connections)
83/// - frontends (bind to a listener)
84/// - backends (to forward connections to)
85/// - clusters (routing rules from frontends to backends)
86/// - TLS certificates
87#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ConfigState {
89    pub clusters: BTreeMap<ClusterId, Cluster>,
90    pub backends: BTreeMap<ClusterId, Vec<Backend>>,
91    /// socket address -> HTTP listener
92    pub http_listeners: BTreeMap<SocketAddr, HttpListenerConfig>,
93    /// socket address -> HTTPS listener
94    pub https_listeners: BTreeMap<SocketAddr, HttpsListenerConfig>,
95    /// socket address -> TCP listener
96    pub tcp_listeners: BTreeMap<SocketAddr, TcpListenerConfig>,
97    /// socket address -> UDP listener
98    pub udp_listeners: BTreeMap<SocketAddr, UdpListenerConfig>,
99    /// HTTP frontends, indexed by a summary of each front's address;hostname;path, for uniqueness.
100    /// For example: `"0.0.0.0:8080;lolcatho.st;P/api"`
101    pub http_fronts: BTreeMap<String, HttpFrontend>,
102    /// indexed by (address, hostname, path)
103    pub https_fronts: BTreeMap<String, HttpFrontend>,
104    pub tcp_fronts: HashMap<ClusterId, Vec<TcpFrontend>>,
105    pub udp_fronts: HashMap<ClusterId, Vec<UdpFrontend>>,
106    pub certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
107    /// A census of requests that were received. Name of the request -> number of occurences
108    pub request_counts: BTreeMap<String, i32>,
109}
110
111impl ConfigState {
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    pub fn dispatch(&mut self, request: &Request) -> Result<(), StateError> {
117        let request_type = match &request.request_type {
118            Some(t) => t,
119            None => return Err(StateError::EmptyRequest),
120        };
121
122        self.increment_request_count(request);
123
124        let result = match request_type {
125            RequestType::AddCluster(cluster) => self.add_cluster(cluster),
126            RequestType::RemoveCluster(cluster_id) => self.remove_cluster(cluster_id),
127            RequestType::AddHttpListener(listener) => self.add_http_listener(listener),
128            RequestType::AddHttpsListener(listener) => self.add_https_listener(listener),
129            RequestType::AddTcpListener(listener) => self.add_tcp_listener(listener),
130            RequestType::AddUdpListener(listener) => self.add_udp_listener(listener),
131            RequestType::RemoveListener(remove) => self.remove_listener(remove),
132            RequestType::ActivateListener(activate) => self.activate_listener(activate),
133            RequestType::DeactivateListener(deactivate) => self.deactivate_listener(deactivate),
134            RequestType::AddHttpFrontend(front) => self.add_http_frontend(front),
135            RequestType::RemoveHttpFrontend(front) => self.remove_http_frontend(front),
136            RequestType::AddCertificate(add) => self.add_certificate(add),
137            RequestType::RemoveCertificate(remove) => self.remove_certificate(remove),
138            RequestType::ReplaceCertificate(replace) => self.replace_certificate(replace),
139            RequestType::AddHttpsFrontend(front) => self.add_https_frontend(front),
140            RequestType::RemoveHttpsFrontend(front) => self.remove_https_frontend(front),
141            RequestType::AddTcpFrontend(front) => self.add_tcp_frontend(front),
142            RequestType::RemoveTcpFrontend(front) => self.remove_tcp_frontend(front),
143            RequestType::AddUdpFrontend(front) => self.add_udp_frontend(front),
144            RequestType::RemoveUdpFrontend(front) => self.remove_udp_frontend(front),
145            RequestType::AddBackend(add_backend) => self.add_backend(add_backend),
146            RequestType::RemoveBackend(backend) => self.remove_backend(backend),
147            RequestType::UpdateHttpListener(patch) => self.update_http_listener(patch),
148            RequestType::UpdateHttpsListener(patch) => self.update_https_listener(patch),
149            RequestType::UpdateTcpListener(patch) => self.update_tcp_listener(patch),
150            RequestType::UpdateUdpListener(patch) => self.update_udp_listener(patch),
151            RequestType::SetHealthCheck(set) => self.set_health_check(set),
152            RequestType::RemoveHealthCheck(cluster_id) => self.remove_health_check(cluster_id),
153
154            // This is to avoid the error message. These request types are
155            // worker-only / runtime-only and do not affect the persisted
156            // ConfigState (e.g., a worker-side global limit set via
157            // SetMaxConnectionsPerIp does NOT survive a worker restart;
158            // operators must mirror the change in the TOML to make it
159            // sticky).
160            RequestType::Logging(_)
161            | RequestType::CountRequests(_)
162            | RequestType::Status(_)
163            | RequestType::SoftStop(_)
164            | RequestType::QueryCertificatesFromWorkers(_)
165            | RequestType::QueryClusterById(_)
166            | RequestType::QueryClustersByDomain(_)
167            | RequestType::QueryMetrics(_)
168            | RequestType::QueryClustersHashes(_)
169            | RequestType::ConfigureMetrics(_)
170            | RequestType::SetMetricDetail(_)
171            | RequestType::ReturnListenSockets(_)
172            | RequestType::SetMaxConnectionsPerIp(_)
173            | RequestType::QueryMaxConnectionsPerIp(_)
174            | RequestType::HardStop(_) => Ok(()),
175
176            _other_request => Err(StateError::UndispatchableRequest),
177        };
178
179        // Run-to-completion postcondition: whatever path `dispatch` took, the
180        // cross-map invariants of the model must hold once it returns. We run
181        // the full sweep on both success and error: a failed mutating handler
182        // (e.g. a duplicate `add_*` or an absent `remove_*`) is required to be
183        // a no-op, so the invariants must be intact regardless of the result.
184        #[cfg(debug_assertions)]
185        self.check_invariants();
186
187        result
188    }
189
190    /// Full cross-map invariant sweep for the control-plane state model.
191    ///
192    /// This is the run-to-completion postcondition called via a
193    /// `#[cfg(debug_assertions)]` guard at the end of [`Self::dispatch`]. It
194    /// encodes the coherence invariants the diff/replay machinery relies on:
195    /// every map entry is self-consistent (a value's stored key/cluster_id
196    /// matches the key it is filed under), and the public accounting helpers
197    /// (`count_frontends`/`count_backends`) agree with the raw map contents.
198    ///
199    /// Compiled out entirely in release builds (no body, no callers).
200    #[cfg(debug_assertions)]
201    fn check_invariants(&self) {
202        // Listener maps: the value's `address` field must match the SocketAddr
203        // key it is filed under, or a hot-upgrade replay (which re-derives the
204        // key from the value) would land the entry under a different key.
205        for (addr, listener) in &self.http_listeners {
206            debug_assert_eq!(
207                SocketAddr::from(listener.address),
208                *addr,
209                "http_listener value address must match its map key"
210            );
211        }
212        for (addr, listener) in &self.https_listeners {
213            debug_assert_eq!(
214                SocketAddr::from(listener.address),
215                *addr,
216                "https_listener value address must match its map key"
217            );
218        }
219        for (addr, listener) in &self.tcp_listeners {
220            debug_assert_eq!(
221                SocketAddr::from(listener.address),
222                *addr,
223                "tcp_listener value address must match its map key"
224            );
225        }
226
227        // Clusters: the value's `cluster_id` must match the key it is filed
228        // under (replay re-keys on `cluster.cluster_id`).
229        for (cluster_id, cluster) in &self.clusters {
230            debug_assert_eq!(
231                &cluster.cluster_id, cluster_id,
232                "cluster value cluster_id must match its map key"
233            );
234        }
235
236        // Backends: grouped by cluster_id. Every backend in a bucket must carry
237        // that bucket's cluster_id (replay groups on `backend.cluster_id`), and
238        // the per-cluster Vec must stay deduplicated on (backend_id, address) —
239        // `add_backend` upserts, so duplicates would mean lost state.
240        for (cluster_id, backends) in &self.backends {
241            for backend in backends {
242                debug_assert_eq!(
243                    &backend.cluster_id, cluster_id,
244                    "backend cluster_id must match its bucket key"
245                );
246            }
247            let unique: HashSet<(&String, &SocketAddr)> = backends
248                .iter()
249                .map(|b| (&b.backend_id, &b.address))
250                .collect();
251            debug_assert_eq!(
252                unique.len(),
253                backends.len(),
254                "backends within a cluster must be unique on (backend_id, address)"
255            );
256        }
257
258        // TCP frontends: grouped by cluster_id. Every frontend in a bucket must
259        // carry that bucket's cluster_id, and `add_tcp_frontend` rejects exact
260        // duplicates so each bucket stays a set.
261        for (cluster_id, fronts) in &self.tcp_fronts {
262            for front in fronts {
263                debug_assert_eq!(
264                    &front.cluster_id, cluster_id,
265                    "tcp_frontend cluster_id must match its bucket key"
266                );
267            }
268            let unique: HashSet<&TcpFrontend> = fronts.iter().collect();
269            debug_assert_eq!(
270                unique.len(),
271                fronts.len(),
272                "tcp frontends within a cluster must be unique"
273            );
274        }
275
276        // Certificates: nested map keyed by (address, fingerprint). The inner
277        // map's fingerprint key is the addressing identity used by diff; we do
278        // not recompute it here (expensive), but the outer/inner structure must
279        // not hold an empty inner map silently produced outside the API — an
280        // empty bucket is a benign no-op for diff/replay, so we only assert the
281        // address-key relationship is preserved by construction (trivially true
282        // for a BTree/HashMap), leaving the costly fingerprint recompute out.
283
284        // Public accounting helpers must agree with the raw maps. These are the
285        // numbers the CLI and metrics surface; a drift here is a real bug.
286        let raw_frontends = self.http_fronts.len()
287            + self.https_fronts.len()
288            + self.count_tcp_frontends_raw()
289            + self.udp_fronts.values().map(|v| v.len()).sum::<usize>();
290        debug_assert_eq!(
291            self.count_frontends(),
292            raw_frontends,
293            "count_frontends must equal the sum of all frontend map entries"
294        );
295        let raw_backends: usize = self.backends.values().map(|v| v.len()).sum();
296        debug_assert_eq!(
297            self.count_backends(),
298            raw_backends,
299            "count_backends must equal the sum of all backend Vec lengths"
300        );
301    }
302
303    /// Raw count of TCP frontends across all clusters. Used by the debug-only
304    /// [`Self::check_invariants`] to cross-check `count_frontends`, and by
305    /// `add_tcp_frontend`'s no-mutation-before-admission accounting.
306    /// Compiled unconditionally (unlike `check_invariants`): it is a trivial
307    /// sum of bucket lengths, and `debug_assert!` arguments still typecheck
308    /// in release builds even though they compile out — a debug-gated helper
309    /// referenced there breaks the release/bench build.
310    fn count_tcp_frontends_raw(&self) -> usize {
311        self.tcp_fronts.values().map(|v| v.len()).sum()
312    }
313
314    /// Increments the count for this request type
315    fn increment_request_count(&mut self, request: &Request) {
316        if let Some(request_type) = &request.request_type {
317            let count = self
318                .request_counts
319                .entry(format_request_type(request_type).to_owned())
320                .or_insert(1);
321            *count += 1;
322        }
323    }
324
325    pub fn get_request_counts(&self) -> RequestCounts {
326        RequestCounts {
327            map: self.request_counts.clone(),
328        }
329    }
330
331    fn add_cluster(&mut self, cluster: &Cluster) -> Result<(), StateError> {
332        // Validate any inline `cluster.health_check` before mutating state so
333        // an invalid config (zero thresholds, missing leading `/`, CR/LF/NUL/C0
334        // in URI) cannot ride in via the AddCluster path. Without this, TOML
335        // reload, SaveState/LoadState, and direct API AddCluster requests
336        // bypass the SetHealthCheck-side check and let an attacker-controlled
337        // health-check URI smuggle CRLF into outbound HTTP/1.1 probes.
338        if let Some(hc) = cluster.health_check.as_ref()
339            && let Err(reason) = crate::config::validate_health_check_config(hc)
340        {
341            return Err(StateError::InvalidValue {
342                field: "health_check",
343                reason,
344            });
345        }
346        let cluster = cluster.clone();
347        // AddCluster is an upsert (replacing an existing cluster_id keeps the
348        // entry count flat), so we assert on presence/key-coherence rather than
349        // a strict +1 on len.
350        let cluster_id = cluster.cluster_id.clone();
351        self.clusters.insert(cluster_id.clone(), cluster);
352        debug_assert!(
353            self.clusters.contains_key(&cluster_id),
354            "add_cluster must leave the cluster present in the map"
355        );
356        debug_assert_eq!(
357            self.clusters.get(&cluster_id).map(|c| &c.cluster_id),
358            Some(&cluster_id),
359            "stored cluster must be keyed by its own cluster_id"
360        );
361        Ok(())
362    }
363
364    fn remove_cluster(&mut self, cluster_id: &str) -> Result<(), StateError> {
365        let before = self.clusters.len();
366        match self.clusters.remove(cluster_id) {
367            Some(_) => {
368                debug_assert!(
369                    !self.clusters.contains_key(cluster_id),
370                    "remove_cluster must evict the cluster"
371                );
372                debug_assert_eq!(
373                    self.clusters.len(),
374                    before - 1,
375                    "remove_cluster must drop exactly one entry"
376                );
377                Ok(())
378            }
379            None => {
380                debug_assert_eq!(
381                    self.clusters.len(),
382                    before,
383                    "a failed remove_cluster must not mutate the map"
384                );
385                Err(StateError::NotFound {
386                    kind: ObjectKind::Cluster,
387                    id: cluster_id.to_owned(),
388                })
389            }
390        }
391    }
392
393    fn set_health_check(&mut self, set: &SetHealthCheck) -> Result<(), StateError> {
394        // Validate before mutating state so an invalid config (zero
395        // thresholds, missing leading `/`, CR/LF/NUL/C0 in URI) cannot
396        // round-trip through SaveState/LoadState. The worker also
397        // validates at the SetHealthCheck handler — this is the
398        // master-side mirror so off-channel TOML reload paths don't
399        // bypass the policy.
400        if let Err(reason) = crate::config::validate_health_check_config(&set.config) {
401            return Err(StateError::InvalidValue {
402                field: "health_check",
403                reason,
404            });
405        }
406        match self.clusters.get_mut(&set.cluster_id) {
407            Some(cluster) => {
408                cluster.health_check = Some(set.config.to_owned());
409                Ok(())
410            }
411            None => Err(StateError::NotFound {
412                kind: ObjectKind::Cluster,
413                id: set.cluster_id.to_owned(),
414            }),
415        }
416    }
417
418    fn remove_health_check(&mut self, cluster_id: &str) -> Result<(), StateError> {
419        match self.clusters.get_mut(cluster_id) {
420            Some(cluster) => {
421                cluster.health_check = None;
422                Ok(())
423            }
424            None => Err(StateError::NotFound {
425                kind: ObjectKind::Cluster,
426                id: cluster_id.to_owned(),
427            }),
428        }
429    }
430
431    pub fn list_health_checks(&self, cluster_id: Option<&str>) -> HealthChecksList {
432        let map = self
433            .clusters
434            .iter()
435            .filter(|(id, _)| cluster_id.is_none_or(|filter| filter == id.as_str()))
436            .filter_map(|(id, cluster)| {
437                cluster
438                    .health_check
439                    .as_ref()
440                    .map(|hc| (id.to_owned(), hc.to_owned()))
441            })
442            .collect();
443        HealthChecksList { map }
444    }
445
446    fn add_http_listener(&mut self, listener: &HttpListenerConfig) -> Result<(), StateError> {
447        let address: SocketAddr = listener.address.into();
448        let before = self.http_listeners.len();
449        match self.http_listeners.entry(address) {
450            BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
451            BTreeMapEntry::Occupied(_) => {
452                debug_assert_eq!(
453                    self.http_listeners.len(),
454                    before,
455                    "a rejected duplicate add_http_listener must not mutate the map"
456                );
457                return Err(StateError::Exists {
458                    kind: ObjectKind::HttpListener,
459                    id: address.to_string(),
460                });
461            }
462        };
463        debug_assert!(
464            self.http_listeners.contains_key(&address),
465            "add_http_listener must insert the listener under its address"
466        );
467        debug_assert_eq!(
468            self.http_listeners.len(),
469            before + 1,
470            "add_http_listener inserts exactly one entry on the vacant path"
471        );
472        Ok(())
473    }
474
475    fn add_https_listener(&mut self, listener: &HttpsListenerConfig) -> Result<(), StateError> {
476        let address: SocketAddr = listener.address.into();
477        let before = self.https_listeners.len();
478        match self.https_listeners.entry(address) {
479            BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
480            BTreeMapEntry::Occupied(_) => {
481                debug_assert_eq!(
482                    self.https_listeners.len(),
483                    before,
484                    "a rejected duplicate add_https_listener must not mutate the map"
485                );
486                return Err(StateError::Exists {
487                    kind: ObjectKind::HttpsListener,
488                    id: address.to_string(),
489                });
490            }
491        };
492        debug_assert!(
493            self.https_listeners.contains_key(&address),
494            "add_https_listener must insert the listener under its address"
495        );
496        debug_assert_eq!(
497            self.https_listeners.len(),
498            before + 1,
499            "add_https_listener inserts exactly one entry on the vacant path"
500        );
501        Ok(())
502    }
503
504    fn add_tcp_listener(&mut self, listener: &TcpListenerConfig) -> Result<(), StateError> {
505        let address: SocketAddr = listener.address.into();
506        let before = self.tcp_listeners.len();
507        match self.tcp_listeners.entry(address) {
508            BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(*listener),
509            BTreeMapEntry::Occupied(_) => {
510                debug_assert_eq!(
511                    self.tcp_listeners.len(),
512                    before,
513                    "a rejected duplicate add_tcp_listener must not mutate the map"
514                );
515                return Err(StateError::Exists {
516                    kind: ObjectKind::TcpListener,
517                    id: address.to_string(),
518                });
519            }
520        };
521        debug_assert!(
522            self.tcp_listeners.contains_key(&address),
523            "add_tcp_listener must insert the listener under its address"
524        );
525        debug_assert_eq!(
526            self.tcp_listeners.len(),
527            before + 1,
528            "add_tcp_listener inserts exactly one entry on the vacant path"
529        );
530        Ok(())
531    }
532
533    fn add_udp_listener(&mut self, listener: &UdpListenerConfig) -> Result<(), StateError> {
534        let address: SocketAddr = listener.address.into();
535        match self.udp_listeners.entry(address) {
536            BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(*listener),
537            BTreeMapEntry::Occupied(_) => {
538                return Err(StateError::Exists {
539                    kind: ObjectKind::UdpListener,
540                    id: address.to_string(),
541                });
542            }
543        };
544        Ok(())
545    }
546
547    fn remove_listener(&mut self, remove: &RemoveListener) -> Result<(), StateError> {
548        match ListenerType::try_from(remove.proxy).map_err(StateError::WrongFieldValue)? {
549            ListenerType::Http => self.remove_http_listener(&remove.address.into()),
550            ListenerType::Https => self.remove_https_listener(&remove.address.into()),
551            ListenerType::Tcp => self.remove_tcp_listener(&remove.address.into()),
552            ListenerType::Udp => self.remove_udp_listener(&remove.address.into()),
553        }
554    }
555
556    fn remove_http_listener(&mut self, address: &SocketAddr) -> Result<(), StateError> {
557        let before = self.http_listeners.len();
558        if self.http_listeners.remove(address).is_none() {
559            debug_assert_eq!(
560                self.http_listeners.len(),
561                before,
562                "a failed remove_http_listener must not mutate the map"
563            );
564            return Err(StateError::NoChange);
565        }
566        debug_assert!(
567            !self.http_listeners.contains_key(address),
568            "remove_http_listener must evict the address"
569        );
570        debug_assert_eq!(
571            self.http_listeners.len(),
572            before - 1,
573            "remove_http_listener drops exactly one entry"
574        );
575        Ok(())
576    }
577
578    fn remove_https_listener(&mut self, address: &SocketAddr) -> Result<(), StateError> {
579        let before = self.https_listeners.len();
580        if self.https_listeners.remove(address).is_none() {
581            debug_assert_eq!(
582                self.https_listeners.len(),
583                before,
584                "a failed remove_https_listener must not mutate the map"
585            );
586            return Err(StateError::NoChange);
587        }
588        debug_assert!(
589            !self.https_listeners.contains_key(address),
590            "remove_https_listener must evict the address"
591        );
592        debug_assert_eq!(
593            self.https_listeners.len(),
594            before - 1,
595            "remove_https_listener drops exactly one entry"
596        );
597        Ok(())
598    }
599
600    fn remove_tcp_listener(&mut self, address: &SocketAddr) -> Result<(), StateError> {
601        let before = self.tcp_listeners.len();
602        if self.tcp_listeners.remove(address).is_none() {
603            debug_assert_eq!(
604                self.tcp_listeners.len(),
605                before,
606                "a failed remove_tcp_listener must not mutate the map"
607            );
608            return Err(StateError::NoChange);
609        }
610        debug_assert!(
611            !self.tcp_listeners.contains_key(address),
612            "remove_tcp_listener must evict the address"
613        );
614        debug_assert_eq!(
615            self.tcp_listeners.len(),
616            before - 1,
617            "remove_tcp_listener drops exactly one entry"
618        );
619        Ok(())
620    }
621
622    fn remove_udp_listener(&mut self, address: &SocketAddr) -> Result<(), StateError> {
623        if self.udp_listeners.remove(address).is_none() {
624            return Err(StateError::NoChange);
625        }
626        Ok(())
627    }
628
629    /// Validate and apply a partial patch to an existing HTTP listener.
630    ///
631    /// Only `Some` fields in the patch are written; `None` fields preserve the
632    /// current value. Returns `StateError::NotFound` if the address is unknown,
633    /// `StateError::InvalidValue` if a flood-knob value is below the required
634    /// minimum.
635    fn update_http_listener(&mut self, patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
636        validate_h2_flood_knobs_http(patch)?;
637
638        let address: SocketAddr = patch.address.into();
639        let listener =
640            self.http_listeners
641                .get_mut(&address)
642                .ok_or_else(|| StateError::NotFound {
643                    kind: ObjectKind::HttpListener,
644                    id: address.to_string(),
645                })?;
646
647        // Shared session-at-accept / per-connection knobs
648        if let Some(v) = patch.public_address {
649            listener.public_address = Some(v);
650        }
651        if let Some(v) = patch.expect_proxy {
652            listener.expect_proxy = v;
653        }
654        if let Some(ref v) = patch.sticky_name {
655            listener.sticky_name = v.to_owned();
656        }
657        if let Some(v) = patch.front_timeout {
658            listener.front_timeout = v;
659        }
660        if let Some(v) = patch.back_timeout {
661            listener.back_timeout = v;
662        }
663        if let Some(v) = patch.connect_timeout {
664            listener.connect_timeout = v;
665        }
666        if let Some(v) = patch.request_timeout {
667            listener.request_timeout = v;
668        }
669        if let Some(patch_answers) = patch.http_answers.as_ref() {
670            merge_custom_http_answers(&mut listener.http_answers, patch_answers);
671        }
672        // H2 flood knobs
673        if let Some(v) = patch.h2_max_rst_stream_per_window {
674            listener.h2_max_rst_stream_per_window = Some(v);
675        }
676        if let Some(v) = patch.h2_max_ping_per_window {
677            listener.h2_max_ping_per_window = Some(v);
678        }
679        if let Some(v) = patch.h2_max_settings_per_window {
680            listener.h2_max_settings_per_window = Some(v);
681        }
682        if let Some(v) = patch.h2_max_empty_data_per_window {
683            listener.h2_max_empty_data_per_window = Some(v);
684        }
685        if let Some(v) = patch.h2_max_continuation_frames {
686            listener.h2_max_continuation_frames = Some(v);
687        }
688        if let Some(v) = patch.h2_max_glitch_count {
689            listener.h2_max_glitch_count = Some(v);
690        }
691        if let Some(v) = patch.h2_initial_connection_window {
692            listener.h2_initial_connection_window = Some(v);
693        }
694        if let Some(v) = patch.h2_max_concurrent_streams {
695            listener.h2_max_concurrent_streams = Some(v);
696        }
697        if let Some(v) = patch.h2_stream_shrink_ratio {
698            listener.h2_stream_shrink_ratio = Some(v);
699        }
700        if let Some(v) = patch.h2_max_rst_stream_lifetime {
701            listener.h2_max_rst_stream_lifetime = Some(v);
702        }
703        if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
704            listener.h2_max_rst_stream_abusive_lifetime = Some(v);
705        }
706        if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
707            listener.h2_max_rst_stream_emitted_lifetime = Some(v);
708        }
709        if let Some(v) = patch.h2_max_header_list_size {
710            listener.h2_max_header_list_size = Some(v);
711        }
712        if let Some(v) = patch.h2_max_header_table_size {
713            listener.h2_max_header_table_size = Some(v);
714        }
715        if let Some(v) = patch.h2_max_header_fields {
716            listener.h2_max_header_fields = Some(v);
717        }
718        if let Some(v) = patch.h2_stream_idle_timeout_seconds {
719            listener.h2_stream_idle_timeout_seconds = Some(v);
720        }
721        // 0 is valid for graceful_shutdown_deadline (means "wait forever")
722        if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
723            listener.h2_graceful_shutdown_deadline_seconds = Some(v);
724        }
725        if let Some(v) = patch.h2_max_window_update_stream0_per_window {
726            listener.h2_max_window_update_stream0_per_window = Some(v);
727        }
728        if let Some(ref v) = patch.sozu_id_header {
729            validate_sozu_id_header(v)?;
730            listener.sozu_id_header = Some(v.to_owned());
731        }
732        Ok(())
733    }
734
735    /// Validate and apply a partial patch to an existing HTTPS listener.
736    ///
737    /// Only `Some` fields in the patch are written; `None` fields preserve the
738    /// current value. Returns `StateError::NotFound` if the address is unknown,
739    /// `StateError::InvalidValue` if a flood-knob value is below the required
740    /// minimum or an ALPN value is unknown.
741    fn update_https_listener(
742        &mut self,
743        patch: &UpdateHttpsListenerConfig,
744    ) -> Result<(), StateError> {
745        validate_h2_flood_knobs_https(patch)?;
746
747        let address: SocketAddr = patch.address.into();
748        let listener =
749            self.https_listeners
750                .get_mut(&address)
751                .ok_or_else(|| StateError::NotFound {
752                    kind: ObjectKind::HttpsListener,
753                    id: address.to_string(),
754                })?;
755
756        // Shared session-at-accept / per-connection knobs
757        if let Some(v) = patch.public_address {
758            listener.public_address = Some(v);
759        }
760        if let Some(v) = patch.expect_proxy {
761            listener.expect_proxy = v;
762        }
763        if let Some(ref v) = patch.sticky_name {
764            listener.sticky_name = v.to_owned();
765        }
766        if let Some(v) = patch.front_timeout {
767            listener.front_timeout = v;
768        }
769        if let Some(v) = patch.back_timeout {
770            listener.back_timeout = v;
771        }
772        if let Some(v) = patch.connect_timeout {
773            listener.connect_timeout = v;
774        }
775        if let Some(v) = patch.request_timeout {
776            listener.request_timeout = v;
777        }
778        if let Some(patch_answers) = patch.http_answers.as_ref() {
779            merge_custom_http_answers(&mut listener.http_answers, patch_answers);
780        }
781        // HTTPS-only knobs
782        if let Some(ref alpn_wrapper) = patch.alpn_protocols {
783            validate_alpn_protocols(&alpn_wrapper.values)?;
784            // Empty values vec = reset to default (runtime treats empty as default)
785            listener.alpn_protocols = alpn_wrapper.values.clone();
786        }
787        if let Some(v) = patch.strict_sni_binding {
788            listener.strict_sni_binding = Some(v);
789        }
790        if let Some(v) = patch.disable_http11 {
791            listener.disable_http11 = Some(v);
792        }
793        // H2 flood knobs
794        if let Some(v) = patch.h2_max_rst_stream_per_window {
795            listener.h2_max_rst_stream_per_window = Some(v);
796        }
797        if let Some(v) = patch.h2_max_ping_per_window {
798            listener.h2_max_ping_per_window = Some(v);
799        }
800        if let Some(v) = patch.h2_max_settings_per_window {
801            listener.h2_max_settings_per_window = Some(v);
802        }
803        if let Some(v) = patch.h2_max_empty_data_per_window {
804            listener.h2_max_empty_data_per_window = Some(v);
805        }
806        if let Some(v) = patch.h2_max_continuation_frames {
807            listener.h2_max_continuation_frames = Some(v);
808        }
809        if let Some(v) = patch.h2_max_glitch_count {
810            listener.h2_max_glitch_count = Some(v);
811        }
812        if let Some(v) = patch.h2_initial_connection_window {
813            listener.h2_initial_connection_window = Some(v);
814        }
815        if let Some(v) = patch.h2_max_concurrent_streams {
816            listener.h2_max_concurrent_streams = Some(v);
817        }
818        if let Some(v) = patch.h2_stream_shrink_ratio {
819            listener.h2_stream_shrink_ratio = Some(v);
820        }
821        if let Some(v) = patch.h2_max_rst_stream_lifetime {
822            listener.h2_max_rst_stream_lifetime = Some(v);
823        }
824        if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
825            listener.h2_max_rst_stream_abusive_lifetime = Some(v);
826        }
827        if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
828            listener.h2_max_rst_stream_emitted_lifetime = Some(v);
829        }
830        if let Some(v) = patch.h2_max_header_list_size {
831            listener.h2_max_header_list_size = Some(v);
832        }
833        if let Some(v) = patch.h2_max_header_table_size {
834            listener.h2_max_header_table_size = Some(v);
835        }
836        if let Some(v) = patch.h2_max_header_fields {
837            listener.h2_max_header_fields = Some(v);
838        }
839        if let Some(v) = patch.h2_stream_idle_timeout_seconds {
840            listener.h2_stream_idle_timeout_seconds = Some(v);
841        }
842        // 0 is valid for graceful_shutdown_deadline (means "wait forever")
843        if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
844            listener.h2_graceful_shutdown_deadline_seconds = Some(v);
845        }
846        if let Some(v) = patch.h2_max_window_update_stream0_per_window {
847            listener.h2_max_window_update_stream0_per_window = Some(v);
848        }
849        if let Some(ref v) = patch.sozu_id_header {
850            validate_sozu_id_header(v)?;
851            listener.sozu_id_header = Some(v.to_owned());
852        }
853        Ok(())
854    }
855
856    /// Validate and apply a partial patch to an existing TCP listener.
857    ///
858    /// Only `Some` fields in the patch are written; `None` fields preserve the
859    /// current value. Returns `StateError::NotFound` if the address is unknown.
860    fn update_tcp_listener(&mut self, patch: &UpdateTcpListenerConfig) -> Result<(), StateError> {
861        let address: SocketAddr = patch.address.into();
862        let listener =
863            self.tcp_listeners
864                .get_mut(&address)
865                .ok_or_else(|| StateError::NotFound {
866                    kind: ObjectKind::TcpListener,
867                    id: address.to_string(),
868                })?;
869
870        if let Some(v) = patch.public_address {
871            listener.public_address = Some(v);
872        }
873        if let Some(v) = patch.expect_proxy {
874            listener.expect_proxy = v;
875        }
876        if let Some(v) = patch.front_timeout {
877            listener.front_timeout = v;
878        }
879        if let Some(v) = patch.back_timeout {
880            listener.back_timeout = v;
881        }
882        if let Some(v) = patch.connect_timeout {
883            listener.connect_timeout = v;
884        }
885        Ok(())
886    }
887
888    /// Validate and apply a partial patch to an existing UDP listener.
889    ///
890    /// Only `Some` fields in the patch are written; `None` fields preserve the
891    /// current value. Returns `StateError::NotFound` if the address is unknown.
892    fn update_udp_listener(&mut self, patch: &UpdateUdpListenerConfig) -> Result<(), StateError> {
893        let address: SocketAddr = patch.address.into();
894        let listener =
895            self.udp_listeners
896                .get_mut(&address)
897                .ok_or_else(|| StateError::NotFound {
898                    kind: ObjectKind::UdpListener,
899                    id: address.to_string(),
900                })?;
901
902        if let Some(v) = patch.public_address {
903            listener.public_address = Some(v);
904        }
905        if let Some(v) = patch.front_timeout {
906            listener.front_timeout = v;
907        }
908        if let Some(v) = patch.back_timeout {
909            listener.back_timeout = v;
910        }
911        if let Some(v) = patch.max_rx_datagram_size {
912            listener.max_rx_datagram_size = v;
913        }
914        if let Some(v) = patch.max_flows {
915            listener.max_flows = v;
916        }
917        Ok(())
918    }
919
920    fn activate_listener(&mut self, activate: &ActivateListener) -> Result<(), StateError> {
921        match ListenerType::try_from(activate.proxy).map_err(StateError::WrongFieldValue)? {
922            ListenerType::Http => self
923                .http_listeners
924                .get_mut(&activate.address.into())
925                .map(|listener| listener.active = true)
926                .ok_or(StateError::NotFound {
927                    kind: ObjectKind::HttpListener,
928                    id: activate.address.to_string(),
929                }),
930            ListenerType::Https => self
931                .https_listeners
932                .get_mut(&activate.address.into())
933                .map(|listener| listener.active = true)
934                .ok_or(StateError::NotFound {
935                    kind: ObjectKind::HttpsListener,
936                    id: activate.address.to_string(),
937                }),
938            ListenerType::Tcp => self
939                .tcp_listeners
940                .get_mut(&activate.address.into())
941                .map(|listener| listener.active = true)
942                .ok_or(StateError::NotFound {
943                    kind: ObjectKind::TcpListener,
944                    id: activate.address.to_string(),
945                }),
946            ListenerType::Udp => self
947                .udp_listeners
948                .get_mut(&activate.address.into())
949                .map(|listener| listener.active = true)
950                .ok_or(StateError::NotFound {
951                    kind: ObjectKind::UdpListener,
952                    id: activate.address.to_string(),
953                }),
954        }
955    }
956
957    fn deactivate_listener(&mut self, deactivate: &DeactivateListener) -> Result<(), StateError> {
958        match ListenerType::try_from(deactivate.proxy).map_err(StateError::WrongFieldValue)? {
959            ListenerType::Http => self
960                .http_listeners
961                .get_mut(&deactivate.address.into())
962                .map(|listener| listener.active = false)
963                .ok_or(StateError::NotFound {
964                    kind: ObjectKind::HttpListener,
965                    id: deactivate.address.to_string(),
966                }),
967            ListenerType::Https => self
968                .https_listeners
969                .get_mut(&deactivate.address.into())
970                .map(|listener| listener.active = false)
971                .ok_or(StateError::NotFound {
972                    kind: ObjectKind::HttpsListener,
973                    id: deactivate.address.to_string(),
974                }),
975            ListenerType::Tcp => self
976                .tcp_listeners
977                .get_mut(&deactivate.address.into())
978                .map(|listener| listener.active = false)
979                .ok_or(StateError::NotFound {
980                    kind: ObjectKind::TcpListener,
981                    id: deactivate.address.to_string(),
982                }),
983            ListenerType::Udp => self
984                .udp_listeners
985                .get_mut(&deactivate.address.into())
986                .map(|listener| listener.active = false)
987                .ok_or(StateError::NotFound {
988                    kind: ObjectKind::UdpListener,
989                    id: deactivate.address.to_string(),
990                }),
991        }
992    }
993
994    fn add_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
995        let front_as_key = front.to_string();
996        let before = self.http_fronts.len();
997
998        match self.http_fronts.entry(front.to_string()) {
999            BTreeMapEntry::Vacant(e) => {
1000                e.insert(front.clone().to_frontend().map_err(|into_error| {
1001                    StateError::FrontendConversion {
1002                        frontend: front_as_key,
1003                        error: into_error.to_string(),
1004                    }
1005                })?)
1006            }
1007            BTreeMapEntry::Occupied(_) => {
1008                debug_assert_eq!(
1009                    self.http_fronts.len(),
1010                    before,
1011                    "a rejected duplicate add_http_frontend must not mutate the map"
1012                );
1013                return Err(StateError::Exists {
1014                    kind: ObjectKind::HttpFrontend,
1015                    id: front.to_string(),
1016                });
1017            }
1018        };
1019        // On the conversion-error path the `?` already returned, so reaching
1020        // here means exactly one entry was inserted under the route key.
1021        debug_assert!(
1022            self.http_fronts.contains_key(&front.to_string()),
1023            "add_http_frontend must insert the route key on success"
1024        );
1025        debug_assert_eq!(
1026            self.http_fronts.len(),
1027            before + 1,
1028            "add_http_frontend inserts exactly one entry on success"
1029        );
1030        Ok(())
1031    }
1032
1033    fn add_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
1034        let front_as_key = front.to_string();
1035        let before = self.https_fronts.len();
1036
1037        match self.https_fronts.entry(front.to_string()) {
1038            BTreeMapEntry::Vacant(e) => {
1039                e.insert(front.clone().to_frontend().map_err(|into_error| {
1040                    StateError::FrontendConversion {
1041                        frontend: front_as_key,
1042                        error: into_error.to_string(),
1043                    }
1044                })?)
1045            }
1046            BTreeMapEntry::Occupied(_) => {
1047                debug_assert_eq!(
1048                    self.https_fronts.len(),
1049                    before,
1050                    "a rejected duplicate add_https_frontend must not mutate the map"
1051                );
1052                return Err(StateError::Exists {
1053                    kind: ObjectKind::HttpsFrontend,
1054                    id: front.to_string(),
1055                });
1056            }
1057        };
1058        debug_assert!(
1059            self.https_fronts.contains_key(&front.to_string()),
1060            "add_https_frontend must insert the route key on success"
1061        );
1062        debug_assert_eq!(
1063            self.https_fronts.len(),
1064            before + 1,
1065            "add_https_frontend inserts exactly one entry on success"
1066        );
1067        Ok(())
1068    }
1069
1070    fn remove_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
1071        let key = front.to_string();
1072        let before = self.http_fronts.len();
1073        self.http_fronts.remove(&key).ok_or(StateError::NotFound {
1074            kind: ObjectKind::HttpFrontend,
1075            id: front.to_string(),
1076        })?;
1077        debug_assert!(
1078            !self.http_fronts.contains_key(&key),
1079            "remove_http_frontend must evict the route key"
1080        );
1081        debug_assert_eq!(
1082            self.http_fronts.len(),
1083            before - 1,
1084            "remove_http_frontend drops exactly one entry"
1085        );
1086        Ok(())
1087    }
1088
1089    fn remove_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
1090        let key = front.to_string();
1091        let before = self.https_fronts.len();
1092        self.https_fronts.remove(&key).ok_or(StateError::NotFound {
1093            kind: ObjectKind::HttpsFrontend,
1094            id: front.to_string(),
1095        })?;
1096        debug_assert!(
1097            !self.https_fronts.contains_key(&key),
1098            "remove_https_frontend must evict the route key"
1099        );
1100        debug_assert_eq!(
1101            self.https_fronts.len(),
1102            before - 1,
1103            "remove_https_frontend drops exactly one entry"
1104        );
1105        Ok(())
1106    }
1107
1108    fn add_certificate(&mut self, add: &AddCertificate) -> Result<(), StateError> {
1109        let fingerprint = add
1110            .certificate
1111            .fingerprint()
1112            .map_err(StateError::AddCertificate)?;
1113
1114        let entry = self.certificates.entry(add.address.into()).or_default();
1115
1116        let mut add = add.clone();
1117        add.certificate
1118            .apply_overriding_names()
1119            .map_err(StateError::AddCertificate)?;
1120
1121        if entry.contains_key(&fingerprint) {
1122            info!(
1123                "Skip loading of certificate '{}' for domain '{}' on listener '{}', the certificate is already present.",
1124                fingerprint,
1125                add.certificate.names.join(", "),
1126                add.address
1127            );
1128            return Ok(());
1129        }
1130
1131        let before = entry.len();
1132        entry.insert(fingerprint.clone(), add.certificate);
1133        debug_assert!(
1134            entry.contains_key(&fingerprint),
1135            "add_certificate must insert the fingerprint under its address"
1136        );
1137        debug_assert_eq!(
1138            entry.len(),
1139            before + 1,
1140            "add_certificate inserts exactly one fingerprint on the new path"
1141        );
1142        Ok(())
1143    }
1144
1145    fn remove_certificate(&mut self, remove: &RemoveCertificate) -> Result<(), StateError> {
1146        let fingerprint = Fingerprint(
1147            hex::decode(&remove.fingerprint)
1148                .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
1149        );
1150
1151        if let Some(index) = self.certificates.get_mut(&remove.address.into()) {
1152            index.remove(&fingerprint);
1153            debug_assert!(
1154                !index.contains_key(&fingerprint),
1155                "remove_certificate must evict the fingerprint when the address is known"
1156            );
1157        }
1158
1159        Ok(())
1160    }
1161
1162    /// - Remove old certificate from certificates, using the old fingerprint
1163    /// - calculate the new fingerprint
1164    /// - insert the new certificate with the new fingerprint as key
1165    /// - check that the new entry is present in the certificates hashmap
1166    fn replace_certificate(&mut self, replace: &ReplaceCertificate) -> Result<(), StateError> {
1167        let replace_address = replace.address.into();
1168        let old_fingerprint = Fingerprint(
1169            hex::decode(&replace.old_fingerprint)
1170                .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
1171        );
1172
1173        self.certificates
1174            .get_mut(&replace_address)
1175            .ok_or(StateError::NotFound {
1176                kind: ObjectKind::Certificate,
1177                id: replace.address.to_string(),
1178            })?
1179            .remove(&old_fingerprint);
1180
1181        let new_fingerprint = Fingerprint(
1182            calculate_fingerprint(replace.new_certificate.certificate.as_bytes()).map_err(
1183                |fingerprint_err| StateError::ReplaceCertificate(fingerprint_err.to_string()),
1184            )?,
1185        );
1186
1187        self.certificates
1188            .get_mut(&replace_address)
1189            .map(|certs| certs.insert(new_fingerprint.clone(), replace.new_certificate.clone()));
1190
1191        if !self
1192            .certificates
1193            .get(&replace_address)
1194            .ok_or(StateError::ReplaceCertificate(
1195                "Unlikely error. This entry in the certificate hashmap should be present"
1196                    .to_string(),
1197            ))?
1198            .contains_key(&new_fingerprint)
1199        {
1200            return Err(StateError::ReplaceCertificate(format!(
1201                "Failed to insert the new certificate for address {}",
1202                replace.address
1203            )));
1204        }
1205        // Postcondition: the new fingerprint is keyed under the address, and
1206        // (unless old and new collide, e.g. a self-replace) the old one is gone.
1207        debug_assert!(
1208            self.certificates
1209                .get(&replace_address)
1210                .is_some_and(|certs| certs.contains_key(&new_fingerprint)),
1211            "replace_certificate must leave the new fingerprint present"
1212        );
1213        debug_assert!(
1214            new_fingerprint == old_fingerprint
1215                || self
1216                    .certificates
1217                    .get(&replace_address)
1218                    .is_none_or(|certs| !certs.contains_key(&old_fingerprint)),
1219            "replace_certificate must evict the old fingerprint unless it equals the new one"
1220        );
1221        Ok(())
1222    }
1223
1224    /// Admission control mirrors the worker's `validate_new_tcp_front`
1225    /// (`lib/src/tcp.rs`) so the master never persists a route its own
1226    /// workers reject on the next fan-out (sozu-proxy/sozu#1290): the
1227    /// command server mutates master state before dispatching to workers,
1228    /// and a worker NACK never rolls the master back. Every check below
1229    /// runs, and every canonicalization is computed, BEFORE `self.tcp_fronts`
1230    /// is touched -- an early `Err` return is a true no-op on `self`.
1231    fn add_tcp_frontend(&mut self, front: &RequestTcpFrontend) -> Result<(), StateError> {
1232        let address: SocketAddr = front.address.into();
1233
1234        // Canonicalize first: shape-validate and lowercase `sni` through the
1235        // SAME `validate_sni_pattern` config-load and the worker use, and
1236        // reduce `alpn` to its canonical sorted+deduped set. Both canonical
1237        // forms are what gets stored (so the state's own hashing/diffing/
1238        // `generate_requests` replay sees one identity per route,
1239        // independent of how the caller cased/ordered its request) AND what
1240        // every admission check below compares against.
1241        let normalized_sni = match &front.sni {
1242            Some(sni) => Some(validate_sni_pattern(sni).map_err(|config_error| {
1243                StateError::InvalidTcpFrontend {
1244                    address,
1245                    reason: format!("sni {sni:?} failed SNI shape validation: {config_error}"),
1246                }
1247            })?),
1248            None => None,
1249        };
1250        let alpn = canonical_tcp_alpn(&front.alpn);
1251
1252        // Worker parity (`validate_new_tcp_front`'s first no-SNI check):
1253        // alpn only matches within an SNI-scoped preread, so a frontend
1254        // without sni would silently ignore its alpn list. Config-load's
1255        // `to_tcp_front` already rejects this shape; a raw `AddTcpFrontend`
1256        // over the command socket must not slip past the master either.
1257        if normalized_sni.is_none() && !alpn.is_empty() {
1258            return Err(StateError::InvalidTcpFrontend {
1259                address,
1260                reason: format!(
1261                    "alpn = {alpn:?} set without sni: alpn only matches within an SNI-scoped \
1262                     preread, so a frontend without sni would silently ignore its alpn list"
1263                ),
1264            });
1265        }
1266
1267        let total_before = self.count_tcp_frontends_raw();
1268
1269        // Listener-wide scan across ALL clusters' buckets at this address --
1270        // the previous check only walked `front.cluster_id`'s own bucket, so
1271        // a duplicate identity, a no-SNI/SNI mix, a second catch-all, or an
1272        // overlapping ALPN set under a DIFFERENT cluster_id all used to pass
1273        // here and then fail on every worker.
1274        let mut listener_has_no_sni_front = false;
1275        let mut listener_has_sni_front = false;
1276        for existing in self
1277            .tcp_fronts
1278            .values()
1279            .flatten()
1280            .filter(|existing| existing.address == address)
1281        {
1282            if tcp_frontend_matches(existing, address, &normalized_sni, &alpn) {
1283                return Err(StateError::Exists {
1284                    kind: ObjectKind::TcpFrontend,
1285                    id: format!(
1286                        "TcpFrontend {{ address: {address}, sni: {normalized_sni:?}, \
1287                         alpn: {alpn:?} }}"
1288                    ),
1289                });
1290            }
1291            match &existing.sni {
1292                None => listener_has_no_sni_front = true,
1293                Some(existing_sni) => {
1294                    listener_has_sni_front = true;
1295                    // Catch-all / ALPN-overlap only matter between two
1296                    // SNI-scoped fronts sharing the exact same normalized
1297                    // sni -- exact and wildcard patterns are DISTINCT keys
1298                    // here, never matched against each other (no trie
1299                    // matching in `ConfigState`), mirroring the worker's own
1300                    // `accept_wildcard: false` self-lookup.
1301                    if normalized_sni.as_deref() != Some(existing_sni.as_str()) {
1302                        continue;
1303                    }
1304                    let existing_is_catch_all = existing.alpn.is_empty();
1305                    let new_is_catch_all = alpn.is_empty();
1306                    if existing_is_catch_all && new_is_catch_all {
1307                        return Err(StateError::InvalidTcpFrontend {
1308                            address,
1309                            reason: format!(
1310                                "sni {existing_sni:?} already has a catch-all (empty alpn) \
1311                                 frontend: at most one frontend per (address, sni) may omit \
1312                                 alpn"
1313                            ),
1314                        });
1315                    }
1316                    // A catch-all coexisting with an ALPN-scoped sibling on
1317                    // the same sni is legal (worker parity: `AlpnMatcher::Any`
1318                    // only conflicts with another `Any`, never with a
1319                    // `OneOf`) -- only two ALPN-scoped fronts can overlap.
1320                    if !existing_is_catch_all
1321                        && !new_is_catch_all
1322                        && let Some(overlap) = alpn
1323                            .iter()
1324                            .find(|protocol| existing.alpn.contains(protocol))
1325                    {
1326                        return Err(StateError::InvalidTcpFrontend {
1327                            address,
1328                            reason: format!(
1329                                "sni {existing_sni:?} already has a frontend matching ALPN \
1330                                 protocol {overlap:?}: ALPN matchers for the same (address, \
1331                                 sni) must not overlap"
1332                            ),
1333                        });
1334                    }
1335                }
1336            }
1337        }
1338        match &normalized_sni {
1339            None if listener_has_sni_front => {
1340                return Err(StateError::InvalidTcpFrontend {
1341                    address,
1342                    reason: "a no-SNI frontend cannot be added to a listener that already has \
1343                             SNI-scoped routes"
1344                        .to_string(),
1345                });
1346            }
1347            Some(_) if listener_has_no_sni_front => {
1348                return Err(StateError::InvalidTcpFrontend {
1349                    address,
1350                    reason: "an SNI-scoped frontend cannot be added to a listener that already \
1351                             has a no-SNI catch-all cluster"
1352                        .to_string(),
1353                });
1354            }
1355            _ => {}
1356        }
1357        // POST: every admission check above either returned `Err` or fell
1358        // through -- none of them may mutate `self`, so the total frontend
1359        // count observed before the scan must still hold at this point.
1360        debug_assert_eq!(
1361            self.count_tcp_frontends_raw(),
1362            total_before,
1363            "add_tcp_frontend must not mutate tcp_fronts before every admission check has \
1364             passed"
1365        );
1366
1367        let tcp_frontend = TcpFrontend {
1368            cluster_id: front.cluster_id.clone(),
1369            address,
1370            tags: front.tags.clone(),
1371            sni: normalized_sni,
1372            alpn,
1373        };
1374        let tcp_frontends = self.tcp_fronts.entry(front.cluster_id.clone()).or_default();
1375        let before = tcp_frontends.len();
1376        debug_assert_eq!(
1377            tcp_frontend.cluster_id, front.cluster_id,
1378            "the built frontend must carry its bucket's cluster_id"
1379        );
1380        tcp_frontends.push(tcp_frontend);
1381        debug_assert_eq!(
1382            tcp_frontends.len(),
1383            before + 1,
1384            "add_tcp_frontend appends exactly one entry"
1385        );
1386        Ok(())
1387    }
1388
1389    fn remove_tcp_frontend(
1390        &mut self,
1391        front_to_remove: &RequestTcpFrontend,
1392    ) -> Result<(), StateError> {
1393        let tcp_frontends =
1394            self.tcp_fronts
1395                .get_mut(&front_to_remove.cluster_id)
1396                .ok_or(StateError::NotFound {
1397                    kind: ObjectKind::TcpFrontend,
1398                    id: format!("{front_to_remove:?}"),
1399                })?;
1400
1401        let len = tcp_frontends.len();
1402        let remove_address: SocketAddr = front_to_remove.address.into();
1403        // Canonicalize for matching, mirroring add_tcp_frontend and the
1404        // worker: lowercase `sni` (the SNI-preread core normalizes to
1405        // lowercase before ever looking a route up, and `TcpFrontend.sni` is
1406        // stored in that same normalized form) and reduce `alpn` to its
1407        // canonical sorted+deduped set, so a request differing only in sni
1408        // case or alpn order still matches the stored frontend instead of
1409        // falling through to `NoChange`. Shape is intentionally NOT
1410        // re-validated here: a malformed pattern could never have been
1411        // stored post-fix, so it simply matches nothing below.
1412        let remove_sni = front_to_remove.sni.as_deref().map(str::to_ascii_lowercase);
1413        let remove_alpn = canonical_tcp_alpn(&front_to_remove.alpn);
1414        // INV: removal identity mirrors add_tcp_frontend's (address, sni,
1415        // alpn) key — removing one SNI-scoped frontend on a listener must
1416        // not also evict a sibling frontend at the same address with a
1417        // different sni/alpn.
1418        tcp_frontends.retain(|front| {
1419            !tcp_frontend_matches(front, remove_address, &remove_sni, &remove_alpn)
1420        });
1421        let after = tcp_frontends.len();
1422        if after == len {
1423            return Err(StateError::NoChange);
1424        }
1425        // `retain` may drop more than one entry only if duplicates on the same
1426        // (address, sni, alpn) ever existed; `add_tcp_frontend` forbids that,
1427        // so a successful removal must drop exactly one and leave none
1428        // matching.
1429        debug_assert_eq!(
1430            after,
1431            len - 1,
1432            "remove_tcp_frontend drops exactly one entry"
1433        );
1434        debug_assert!(
1435            !tcp_frontends.iter().any(|f| tcp_frontend_matches(
1436                f,
1437                remove_address,
1438                &remove_sni,
1439                &remove_alpn
1440            )),
1441            "remove_tcp_frontend must leave no frontend matching the removed (address, sni, alpn)"
1442        );
1443        Ok(())
1444    }
1445
1446    fn add_udp_frontend(&mut self, front: &RequestUdpFrontend) -> Result<(), StateError> {
1447        let udp_frontends = self.udp_fronts.entry(front.cluster_id.clone()).or_default();
1448
1449        let udp_frontend = UdpFrontend {
1450            cluster_id: front.cluster_id.clone(),
1451            address: front.address.into(),
1452            tags: front.tags.clone(),
1453        };
1454        if udp_frontends.contains(&udp_frontend) {
1455            return Err(StateError::Exists {
1456                kind: ObjectKind::UdpFrontend,
1457                id: format!("{udp_frontend:?}"),
1458            });
1459        }
1460
1461        udp_frontends.push(udp_frontend);
1462        Ok(())
1463    }
1464
1465    fn remove_udp_frontend(
1466        &mut self,
1467        front_to_remove: &RequestUdpFrontend,
1468    ) -> Result<(), StateError> {
1469        let udp_frontends =
1470            self.udp_fronts
1471                .get_mut(&front_to_remove.cluster_id)
1472                .ok_or(StateError::NotFound {
1473                    kind: ObjectKind::UdpFrontend,
1474                    id: format!("{front_to_remove:?}"),
1475                })?;
1476
1477        let len = udp_frontends.len();
1478        udp_frontends.retain(|front| front.address != front_to_remove.address.into());
1479        if udp_frontends.len() == len {
1480            return Err(StateError::NoChange);
1481        }
1482        Ok(())
1483    }
1484
1485    fn add_backend(&mut self, add_backend: &AddBackend) -> Result<(), StateError> {
1486        let backend = Backend {
1487            address: add_backend.address.into(),
1488            cluster_id: add_backend.cluster_id.clone(),
1489            backend_id: add_backend.backend_id.clone(),
1490            sticky_id: add_backend.sticky_id.clone(),
1491            load_balancing_parameters: add_backend.load_balancing_parameters,
1492            backup: add_backend.backup,
1493        };
1494        let backends = self.backends.entry(backend.cluster_id.clone()).or_default();
1495        let backend_id = backend.backend_id.clone();
1496        let backend_address = backend.address;
1497        let before = backends.len();
1498
1499        // we might be modifying the sticky id or load balancing parameters:
1500        // the retain drops at most one prior copy (the map stays deduplicated
1501        // on (backend_id, address)), then we re-push the new version. So the
1502        // net length grows by exactly one iff this was a brand-new backend.
1503        let was_present = backends
1504            .iter()
1505            .any(|b| b.backend_id == backend_id && b.address == backend_address);
1506        backends.retain(|b| b.backend_id != backend.backend_id || b.address != backend.address);
1507        debug_assert_eq!(
1508            backends.len(),
1509            before - was_present as usize,
1510            "the upsert retain must drop exactly the prior copy iff it existed"
1511        );
1512        backends.push(backend);
1513        backends.sort();
1514
1515        debug_assert_eq!(
1516            backends.len(),
1517            before + (!was_present) as usize,
1518            "add_backend grows the bucket by one iff the backend was new"
1519        );
1520        debug_assert_eq!(
1521            backends
1522                .iter()
1523                .filter(|b| b.backend_id == backend_id && b.address == backend_address)
1524                .count(),
1525            1,
1526            "exactly one copy of the upserted backend must remain"
1527        );
1528        Ok(())
1529    }
1530
1531    fn remove_backend(&mut self, backend: &RemoveBackend) -> Result<(), StateError> {
1532        let backend_list =
1533            self.backends
1534                .get_mut(&backend.cluster_id)
1535                .ok_or(StateError::NotFound {
1536                    kind: ObjectKind::Backend,
1537                    id: backend.backend_id.to_owned(),
1538                })?;
1539
1540        let len = backend_list.len();
1541        let remove_address: SocketAddr = backend.address.into();
1542        backend_list.retain(|b| b.backend_id != backend.backend_id || b.address != remove_address);
1543        backend_list.sort();
1544        let after = backend_list.len();
1545        if after == len {
1546            return Err(StateError::NoChange);
1547        }
1548        // The list is deduplicated on (backend_id, address), so a matching
1549        // removal drops exactly one entry and leaves nothing matching.
1550        debug_assert_eq!(after, len - 1, "remove_backend drops exactly one entry");
1551        debug_assert!(
1552            !backend_list
1553                .iter()
1554                .any(|b| b.backend_id == backend.backend_id && b.address == remove_address),
1555            "remove_backend must leave no backend matching (backend_id, address)"
1556        );
1557        Ok(())
1558    }
1559
1560    /// creates all requests needed to bootstrap the state
1561    fn generate_requests(&self) -> Vec<Request> {
1562        let mut v: Vec<Request> = Vec::new();
1563
1564        for listener in self.http_listeners.values() {
1565            v.push(RequestType::AddHttpListener(listener.clone()).into());
1566            if listener.active {
1567                v.push(
1568                    RequestType::ActivateListener(ActivateListener {
1569                        address: listener.address,
1570                        proxy: ListenerType::Http.into(),
1571                        from_scm: false,
1572                    })
1573                    .into(),
1574                );
1575            }
1576        }
1577
1578        for listener in self.https_listeners.values() {
1579            v.push(RequestType::AddHttpsListener(listener.clone()).into());
1580            if listener.active {
1581                v.push(
1582                    RequestType::ActivateListener(ActivateListener {
1583                        address: listener.address,
1584                        proxy: ListenerType::Https.into(),
1585                        from_scm: false,
1586                    })
1587                    .into(),
1588                );
1589            }
1590        }
1591
1592        for listener in self.tcp_listeners.values() {
1593            v.push(RequestType::AddTcpListener(*listener).into());
1594            if listener.active {
1595                v.push(
1596                    RequestType::ActivateListener(ActivateListener {
1597                        address: listener.address,
1598                        proxy: ListenerType::Tcp.into(),
1599                        from_scm: false,
1600                    })
1601                    .into(),
1602                );
1603            }
1604        }
1605
1606        for listener in self.udp_listeners.values() {
1607            v.push(RequestType::AddUdpListener(*listener).into());
1608            if listener.active {
1609                v.push(
1610                    RequestType::ActivateListener(ActivateListener {
1611                        address: listener.address,
1612                        proxy: ListenerType::Udp.into(),
1613                        from_scm: false,
1614                    })
1615                    .into(),
1616                );
1617            }
1618        }
1619
1620        for cluster in self.clusters.values() {
1621            v.push(RequestType::AddCluster(cluster.clone()).into());
1622        }
1623
1624        for front in self.http_fronts.values() {
1625            v.push(RequestType::AddHttpFrontend(front.clone().into()).into());
1626        }
1627
1628        for (front, certs) in self.certificates.iter() {
1629            for certificate_and_key in certs.values() {
1630                v.push(
1631                    RequestType::AddCertificate(AddCertificate {
1632                        address: SocketAddress::from(*front),
1633                        certificate: certificate_and_key.clone(),
1634                        expired_at: None,
1635                    })
1636                    .into(),
1637                );
1638            }
1639        }
1640
1641        for front in self.https_fronts.values() {
1642            v.push(RequestType::AddHttpsFrontend(front.clone().into()).into());
1643        }
1644
1645        for front_list in self.tcp_fronts.values() {
1646            for front in front_list {
1647                v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
1648            }
1649        }
1650
1651        for front_list in self.udp_fronts.values() {
1652            for front in front_list {
1653                v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
1654            }
1655        }
1656
1657        for backend_list in self.backends.values() {
1658            for backend in backend_list {
1659                v.push(RequestType::AddBackend(backend.clone().to_add_backend()).into());
1660            }
1661        }
1662
1663        // Bootstrap round-trip: replaying `generate_requests` into a fresh,
1664        // empty `ConfigState` must reconstruct `self`'s maps exactly. This is
1665        // the property SaveState/LoadState and worker bootstrap depend on. We
1666        // compare the business maps only (not `request_counts`, which is
1667        // bookkeeping mutated by `dispatch`).
1668        #[cfg(debug_assertions)]
1669        {
1670            let mut replayed = ConfigState::new();
1671            for request in &v {
1672                debug_assert!(
1673                    replayed.dispatch(request).is_ok(),
1674                    "every request from generate_requests must replay cleanly"
1675                );
1676            }
1677            debug_assert!(
1678                replayed.clusters == self.clusters
1679                    && replayed.backends == self.backends
1680                    && replayed.http_listeners == self.http_listeners
1681                    && replayed.https_listeners == self.https_listeners
1682                    && replayed.tcp_listeners == self.tcp_listeners
1683                    && replayed.http_fronts == self.http_fronts
1684                    && replayed.https_fronts == self.https_fronts
1685                    && replayed.tcp_fronts == self.tcp_fronts
1686                    && replayed.certificates == self.certificates,
1687                "replaying generate_requests into a fresh state must reproduce self"
1688            );
1689        }
1690
1691        v
1692    }
1693
1694    pub fn generate_activate_requests(&self) -> Vec<Request> {
1695        let mut v: Vec<Request> = Vec::new();
1696        for front in self
1697            .http_listeners
1698            .iter()
1699            .filter(|(_, listener)| listener.active)
1700            .map(|(k, _)| k)
1701        {
1702            v.push(
1703                RequestType::ActivateListener(ActivateListener {
1704                    address: SocketAddress::from(*front),
1705                    proxy: ListenerType::Http.into(),
1706                    from_scm: false,
1707                })
1708                .into(),
1709            );
1710        }
1711
1712        for front in self
1713            .https_listeners
1714            .iter()
1715            .filter(|(_, listener)| listener.active)
1716            .map(|(k, _)| k)
1717        {
1718            v.push(
1719                RequestType::ActivateListener(ActivateListener {
1720                    address: SocketAddress::from(*front),
1721                    proxy: ListenerType::Https.into(),
1722                    from_scm: false,
1723                })
1724                .into(),
1725            );
1726        }
1727        for front in self
1728            .tcp_listeners
1729            .iter()
1730            .filter(|(_, listener)| listener.active)
1731            .map(|(k, _)| k)
1732        {
1733            v.push(
1734                RequestType::ActivateListener(ActivateListener {
1735                    address: SocketAddress::from(*front),
1736                    proxy: ListenerType::Tcp.into(),
1737                    from_scm: false,
1738                })
1739                .into(),
1740            );
1741        }
1742        for front in self
1743            .udp_listeners
1744            .iter()
1745            .filter(|(_, listener)| listener.active)
1746            .map(|(k, _)| k)
1747        {
1748            v.push(
1749                RequestType::ActivateListener(ActivateListener {
1750                    address: SocketAddress::from(*front),
1751                    proxy: ListenerType::Udp.into(),
1752                    from_scm: false,
1753                })
1754                .into(),
1755            );
1756        }
1757
1758        // Symmetry with the active-listener census: exactly one ActivateListener
1759        // is emitted per active listener across the four maps, no more, no less.
1760        #[cfg(debug_assertions)]
1761        {
1762            let active_listeners = self.http_listeners.values().filter(|l| l.active).count()
1763                + self.https_listeners.values().filter(|l| l.active).count()
1764                + self.tcp_listeners.values().filter(|l| l.active).count()
1765                + self.udp_listeners.values().filter(|l| l.active).count();
1766            debug_assert_eq!(
1767                v.len(),
1768                active_listeners,
1769                "generate_activate_requests emits one request per active listener"
1770            );
1771            debug_assert!(
1772                v.iter()
1773                    .all(|r| matches!(r.request_type, Some(RequestType::ActivateListener(_)))),
1774                "generate_activate_requests must emit only ActivateListener requests"
1775            );
1776        }
1777
1778        v
1779    }
1780
1781    pub fn diff(&self, other: &ConfigState) -> Vec<Request> {
1782        //pub tcp_listeners:   HashMap<SocketAddr, (TcpListener, bool)>,
1783        let my_tcp_listeners: HashSet<&SocketAddr> = self.tcp_listeners.keys().collect();
1784        let their_tcp_listeners: HashSet<&SocketAddr> = other.tcp_listeners.keys().collect();
1785        let removed_tcp_listeners = my_tcp_listeners.difference(&their_tcp_listeners);
1786        let added_tcp_listeners = their_tcp_listeners.difference(&my_tcp_listeners);
1787
1788        let my_udp_listeners: HashSet<&SocketAddr> = self.udp_listeners.keys().collect();
1789        let their_udp_listeners: HashSet<&SocketAddr> = other.udp_listeners.keys().collect();
1790        let removed_udp_listeners = my_udp_listeners.difference(&their_udp_listeners);
1791        let added_udp_listeners = their_udp_listeners.difference(&my_udp_listeners);
1792
1793        let my_http_listeners: HashSet<&SocketAddr> = self.http_listeners.keys().collect();
1794        let their_http_listeners: HashSet<&SocketAddr> = other.http_listeners.keys().collect();
1795        let removed_http_listeners = my_http_listeners.difference(&their_http_listeners);
1796        let added_http_listeners = their_http_listeners.difference(&my_http_listeners);
1797
1798        let my_https_listeners: HashSet<&SocketAddr> = self.https_listeners.keys().collect();
1799        let their_https_listeners: HashSet<&SocketAddr> = other.https_listeners.keys().collect();
1800        let removed_https_listeners = my_https_listeners.difference(&their_https_listeners);
1801        let added_https_listeners = their_https_listeners.difference(&my_https_listeners);
1802
1803        let mut v: Vec<Request> = vec![];
1804
1805        for address in removed_tcp_listeners {
1806            if self.tcp_listeners[*address].active {
1807                v.push(
1808                    RequestType::DeactivateListener(DeactivateListener {
1809                        address: SocketAddress::from(**address),
1810                        proxy: ListenerType::Tcp.into(),
1811                        to_scm: false,
1812                    })
1813                    .into(),
1814                );
1815            }
1816
1817            v.push(
1818                RequestType::RemoveListener(RemoveListener {
1819                    address: SocketAddress::from(**address),
1820                    proxy: ListenerType::Tcp.into(),
1821                })
1822                .into(),
1823            );
1824        }
1825
1826        for address in added_tcp_listeners.clone() {
1827            v.push(RequestType::AddTcpListener(other.tcp_listeners[*address]).into());
1828
1829            if other.tcp_listeners[*address].active {
1830                v.push(
1831                    RequestType::ActivateListener(ActivateListener {
1832                        address: SocketAddress::from(**address),
1833                        proxy: ListenerType::Tcp.into(),
1834                        from_scm: false,
1835                    })
1836                    .into(),
1837                );
1838            }
1839        }
1840
1841        for address in removed_udp_listeners {
1842            if self.udp_listeners[*address].active {
1843                v.push(
1844                    RequestType::DeactivateListener(DeactivateListener {
1845                        address: SocketAddress::from(**address),
1846                        proxy: ListenerType::Udp.into(),
1847                        to_scm: false,
1848                    })
1849                    .into(),
1850                );
1851            }
1852
1853            v.push(
1854                RequestType::RemoveListener(RemoveListener {
1855                    address: SocketAddress::from(**address),
1856                    proxy: ListenerType::Udp.into(),
1857                })
1858                .into(),
1859            );
1860        }
1861
1862        for address in added_udp_listeners.clone() {
1863            v.push(RequestType::AddUdpListener(other.udp_listeners[*address]).into());
1864
1865            if other.udp_listeners[*address].active {
1866                v.push(
1867                    RequestType::ActivateListener(ActivateListener {
1868                        address: SocketAddress::from(**address),
1869                        proxy: ListenerType::Udp.into(),
1870                        from_scm: false,
1871                    })
1872                    .into(),
1873                );
1874            }
1875        }
1876
1877        for address in removed_http_listeners {
1878            if self.http_listeners[*address].active {
1879                v.push(
1880                    RequestType::DeactivateListener(DeactivateListener {
1881                        address: SocketAddress::from(**address),
1882                        proxy: ListenerType::Http.into(),
1883                        to_scm: false,
1884                    })
1885                    .into(),
1886                );
1887            }
1888
1889            v.push(
1890                RequestType::RemoveListener(RemoveListener {
1891                    address: SocketAddress::from(**address),
1892                    proxy: ListenerType::Http.into(),
1893                })
1894                .into(),
1895            );
1896        }
1897
1898        for address in added_http_listeners.clone() {
1899            v.push(RequestType::AddHttpListener(other.http_listeners[*address].clone()).into());
1900
1901            if other.http_listeners[*address].active {
1902                v.push(
1903                    RequestType::ActivateListener(ActivateListener {
1904                        address: SocketAddress::from(**address),
1905                        proxy: ListenerType::Http.into(),
1906                        from_scm: false,
1907                    })
1908                    .into(),
1909                );
1910            }
1911        }
1912
1913        for address in removed_https_listeners {
1914            if self.https_listeners[*address].active {
1915                v.push(
1916                    RequestType::DeactivateListener(DeactivateListener {
1917                        address: SocketAddress::from(**address),
1918                        proxy: ListenerType::Https.into(),
1919                        to_scm: false,
1920                    })
1921                    .into(),
1922                );
1923            }
1924
1925            v.push(
1926                RequestType::RemoveListener(RemoveListener {
1927                    address: SocketAddress::from(**address),
1928                    proxy: ListenerType::Https.into(),
1929                })
1930                .into(),
1931            );
1932        }
1933
1934        for address in added_https_listeners.clone() {
1935            v.push(RequestType::AddHttpsListener(other.https_listeners[*address].clone()).into());
1936
1937            if other.https_listeners[*address].active {
1938                v.push(
1939                    RequestType::ActivateListener(ActivateListener {
1940                        address: SocketAddress::from(**address),
1941                        proxy: ListenerType::Https.into(),
1942                        from_scm: false,
1943                    })
1944                    .into(),
1945                );
1946            }
1947        }
1948
1949        for addr in my_tcp_listeners.intersection(&their_tcp_listeners) {
1950            let my_listener = &self.tcp_listeners[*addr];
1951            let their_listener = &other.tcp_listeners[*addr];
1952
1953            if my_listener != their_listener {
1954                v.push(
1955                    RequestType::RemoveListener(RemoveListener {
1956                        address: SocketAddress::from(**addr),
1957                        proxy: ListenerType::Tcp.into(),
1958                    })
1959                    .into(),
1960                );
1961                // any added listener should be unactive
1962                let mut listener_to_add = *their_listener;
1963                listener_to_add.active = false;
1964                v.push(RequestType::AddTcpListener(listener_to_add).into());
1965
1966                // The Remove + Add(active=false) above wipes the listener's
1967                // active state. Re-emit an ActivateListener whenever the target
1968                // state keeps it active, otherwise a config change on a still
1969                // active listener would silently deactivate it on replay. This
1970                // subsumes the newly-active (`!my.active && their.active`) case:
1971                // a differing `active` flag always makes the listeners unequal.
1972                if their_listener.active {
1973                    v.push(
1974                        RequestType::ActivateListener(ActivateListener {
1975                            address: SocketAddress::from(**addr),
1976                            proxy: ListenerType::Tcp.into(),
1977                            from_scm: false,
1978                        })
1979                        .into(),
1980                    );
1981                }
1982            }
1983
1984            if my_listener.active && !their_listener.active {
1985                v.push(
1986                    RequestType::DeactivateListener(DeactivateListener {
1987                        address: SocketAddress::from(**addr),
1988                        proxy: ListenerType::Tcp.into(),
1989                        to_scm: false,
1990                    })
1991                    .into(),
1992                );
1993            }
1994        }
1995
1996        for addr in my_udp_listeners.intersection(&their_udp_listeners) {
1997            let my_listener = &self.udp_listeners[*addr];
1998            let their_listener = &other.udp_listeners[*addr];
1999
2000            if my_listener != their_listener {
2001                v.push(
2002                    RequestType::RemoveListener(RemoveListener {
2003                        address: SocketAddress::from(**addr),
2004                        proxy: ListenerType::Udp.into(),
2005                    })
2006                    .into(),
2007                );
2008                // any added listener should be unactive
2009                let mut listener_to_add = *their_listener;
2010                listener_to_add.active = false;
2011                v.push(RequestType::AddUdpListener(listener_to_add).into());
2012
2013                // The Remove + Add(active=false) above wipes the listener's
2014                // active state. Re-emit an ActivateListener whenever the target
2015                // state keeps it active, otherwise a config change on a still
2016                // active listener would silently deactivate it on replay. This
2017                // subsumes the newly-active (`!my.active && their.active`) case:
2018                // a differing `active` flag always makes the listeners unequal.
2019                if their_listener.active {
2020                    v.push(
2021                        RequestType::ActivateListener(ActivateListener {
2022                            address: SocketAddress::from(**addr),
2023                            proxy: ListenerType::Udp.into(),
2024                            from_scm: false,
2025                        })
2026                        .into(),
2027                    );
2028                }
2029            }
2030
2031            if my_listener.active && !their_listener.active {
2032                v.push(
2033                    RequestType::DeactivateListener(DeactivateListener {
2034                        address: SocketAddress::from(**addr),
2035                        proxy: ListenerType::Udp.into(),
2036                        to_scm: false,
2037                    })
2038                    .into(),
2039                );
2040            }
2041        }
2042
2043        for addr in my_http_listeners.intersection(&their_http_listeners) {
2044            let my_listener = &self.http_listeners[*addr];
2045            let their_listener = &other.http_listeners[*addr];
2046
2047            if my_listener != their_listener {
2048                v.push(
2049                    RequestType::RemoveListener(RemoveListener {
2050                        address: SocketAddress::from(**addr),
2051                        proxy: ListenerType::Http.into(),
2052                    })
2053                    .into(),
2054                );
2055                // any added listener should be unactive
2056                let mut listener_to_add = their_listener.clone();
2057                listener_to_add.active = false;
2058                v.push(RequestType::AddHttpListener(listener_to_add).into());
2059
2060                // The Remove + Add(active=false) above wipes the listener's
2061                // active state. Re-emit an ActivateListener whenever the target
2062                // state keeps it active, otherwise a config change on a still
2063                // active listener would silently deactivate it on replay. This
2064                // subsumes the newly-active (`!my.active && their.active`) case:
2065                // a differing `active` flag always makes the listeners unequal.
2066                if their_listener.active {
2067                    v.push(
2068                        RequestType::ActivateListener(ActivateListener {
2069                            address: SocketAddress::from(**addr),
2070                            proxy: ListenerType::Http.into(),
2071                            from_scm: false,
2072                        })
2073                        .into(),
2074                    );
2075                }
2076            }
2077
2078            if my_listener.active && !their_listener.active {
2079                v.push(
2080                    RequestType::DeactivateListener(DeactivateListener {
2081                        address: SocketAddress::from(**addr),
2082                        proxy: ListenerType::Http.into(),
2083                        to_scm: false,
2084                    })
2085                    .into(),
2086                );
2087            }
2088        }
2089
2090        for addr in my_https_listeners.intersection(&their_https_listeners) {
2091            let my_listener = &self.https_listeners[*addr];
2092            let their_listener = &other.https_listeners[*addr];
2093
2094            if my_listener != their_listener {
2095                v.push(
2096                    RequestType::RemoveListener(RemoveListener {
2097                        address: SocketAddress::from(**addr),
2098                        proxy: ListenerType::Https.into(),
2099                    })
2100                    .into(),
2101                );
2102                // any added listener should be unactive
2103                let mut listener_to_add = their_listener.clone();
2104                listener_to_add.active = false;
2105                v.push(RequestType::AddHttpsListener(listener_to_add).into());
2106
2107                // The Remove + Add(active=false) above wipes the listener's
2108                // active state. Re-emit an ActivateListener whenever the target
2109                // state keeps it active, otherwise a config change on a still
2110                // active listener would silently deactivate it on replay. This
2111                // subsumes the newly-active (`!my.active && their.active`) case:
2112                // a differing `active` flag always makes the listeners unequal.
2113                if their_listener.active {
2114                    v.push(
2115                        RequestType::ActivateListener(ActivateListener {
2116                            address: SocketAddress::from(**addr),
2117                            proxy: ListenerType::Https.into(),
2118                            from_scm: false,
2119                        })
2120                        .into(),
2121                    );
2122                }
2123            }
2124
2125            if my_listener.active && !their_listener.active {
2126                v.push(
2127                    RequestType::DeactivateListener(DeactivateListener {
2128                        address: SocketAddress::from(**addr),
2129                        proxy: ListenerType::Https.into(),
2130                        to_scm: false,
2131                    })
2132                    .into(),
2133                );
2134            }
2135        }
2136
2137        for (cluster_id, res) in diff_map(self.clusters.iter(), other.clusters.iter()) {
2138            match res {
2139                DiffResult::Added | DiffResult::Changed => v.push(
2140                    RequestType::AddCluster(other.clusters.get(cluster_id).unwrap().clone()).into(),
2141                ),
2142                DiffResult::Removed => {
2143                    v.push(RequestType::RemoveCluster(cluster_id.to_string()).into())
2144                }
2145            }
2146        }
2147
2148        for ((cluster_id, backend_id), res) in diff_map(
2149            self.backends.iter().flat_map(|(cluster_id, v)| {
2150                v.iter()
2151                    .map(move |backend| ((cluster_id, &backend.backend_id), backend))
2152            }),
2153            other.backends.iter().flat_map(|(cluster_id, v)| {
2154                v.iter()
2155                    .map(move |backend| ((cluster_id, &backend.backend_id), backend))
2156            }),
2157        ) {
2158            match res {
2159                DiffResult::Added => {
2160                    let backend = other
2161                        .backends
2162                        .get(cluster_id)
2163                        .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
2164                        .unwrap();
2165                    v.push(RequestType::AddBackend(backend.clone().to_add_backend()).into());
2166                }
2167                DiffResult::Removed => {
2168                    let backend = self
2169                        .backends
2170                        .get(cluster_id)
2171                        .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
2172                        .unwrap();
2173
2174                    v.push(
2175                        RequestType::RemoveBackend(RemoveBackend {
2176                            cluster_id: backend.cluster_id.clone(),
2177                            backend_id: backend.backend_id.clone(),
2178                            address: SocketAddress::from(backend.address),
2179                        })
2180                        .into(),
2181                    );
2182                }
2183                DiffResult::Changed => {
2184                    let backend = self
2185                        .backends
2186                        .get(cluster_id)
2187                        .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
2188                        .unwrap();
2189
2190                    v.push(
2191                        RequestType::RemoveBackend(RemoveBackend {
2192                            cluster_id: backend.cluster_id.clone(),
2193                            backend_id: backend.backend_id.clone(),
2194                            address: SocketAddress::from(backend.address),
2195                        })
2196                        .into(),
2197                    );
2198
2199                    let backend = other
2200                        .backends
2201                        .get(cluster_id)
2202                        .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
2203                        .unwrap();
2204                    v.push(RequestType::AddBackend(backend.clone().to_add_backend()).into());
2205                }
2206            }
2207        }
2208
2209        let mut my_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
2210        for (route, front) in self.http_fronts.iter() {
2211            my_http_fronts.insert((route, front));
2212        }
2213        let mut their_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
2214        for (route, front) in other.http_fronts.iter() {
2215            their_http_fronts.insert((route, front));
2216        }
2217
2218        let removed_http_fronts = my_http_fronts.difference(&their_http_fronts);
2219        let added_http_fronts = their_http_fronts.difference(&my_http_fronts);
2220
2221        for &(_, front) in removed_http_fronts {
2222            v.push(RequestType::RemoveHttpFrontend(front.clone().into()).into());
2223        }
2224
2225        for &(_, front) in added_http_fronts {
2226            v.push(RequestType::AddHttpFrontend(front.clone().into()).into());
2227        }
2228
2229        let mut my_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
2230        for (route, front) in self.https_fronts.iter() {
2231            my_https_fronts.insert((route, front));
2232        }
2233        let mut their_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
2234        for (route, front) in other.https_fronts.iter() {
2235            their_https_fronts.insert((route, front));
2236        }
2237        let removed_https_fronts = my_https_fronts.difference(&their_https_fronts);
2238        let added_https_fronts = their_https_fronts.difference(&my_https_fronts);
2239
2240        for &(_, front) in removed_https_fronts {
2241            v.push(RequestType::RemoveHttpsFrontend(front.clone().into()).into());
2242        }
2243
2244        for &(_, front) in added_https_fronts {
2245            v.push(RequestType::AddHttpsFrontend(front.clone().into()).into());
2246        }
2247
2248        let mut my_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
2249        for (cluster_id, front_list) in self.tcp_fronts.iter() {
2250            for front in front_list.iter() {
2251                my_tcp_fronts.insert((cluster_id, front));
2252            }
2253        }
2254        let mut their_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
2255        for (cluster_id, front_list) in other.tcp_fronts.iter() {
2256            for front in front_list.iter() {
2257                their_tcp_fronts.insert((cluster_id, front));
2258            }
2259        }
2260
2261        let removed_tcp_fronts = my_tcp_fronts.difference(&their_tcp_fronts);
2262        let added_tcp_fronts = their_tcp_fronts.difference(&my_tcp_fronts);
2263
2264        for &(_, front) in removed_tcp_fronts {
2265            v.push(RequestType::RemoveTcpFrontend(front.clone().into()).into());
2266        }
2267
2268        for &(_, front) in added_tcp_fronts {
2269            v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
2270        }
2271
2272        let mut my_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
2273        for (cluster_id, front_list) in self.udp_fronts.iter() {
2274            for front in front_list.iter() {
2275                my_udp_fronts.insert((cluster_id, front));
2276            }
2277        }
2278        let mut their_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
2279        for (cluster_id, front_list) in other.udp_fronts.iter() {
2280            for front in front_list.iter() {
2281                their_udp_fronts.insert((cluster_id, front));
2282            }
2283        }
2284
2285        let removed_udp_fronts = my_udp_fronts.difference(&their_udp_fronts);
2286        let added_udp_fronts = their_udp_fronts.difference(&my_udp_fronts);
2287
2288        for &(_, front) in removed_udp_fronts {
2289            v.push(RequestType::RemoveUdpFrontend(front.clone().into()).into());
2290        }
2291
2292        for &(_, front) in added_udp_fronts {
2293            v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
2294        }
2295
2296        //pub certificates:    HashMap<SocketAddr, HashMap<CertificateFingerprint, (CertificateAndKey, Vec<String>)>>,
2297        let my_certificates: HashSet<(SocketAddr, &Fingerprint)> = HashSet::from_iter(
2298            self.certificates
2299                .iter()
2300                .flat_map(|(addr, certs)| repeat(*addr).zip(certs.keys())),
2301        );
2302        let their_certificates: HashSet<(SocketAddr, &Fingerprint)> = HashSet::from_iter(
2303            other
2304                .certificates
2305                .iter()
2306                .flat_map(|(addr, certs)| repeat(*addr).zip(certs.keys())),
2307        );
2308
2309        let removed_certificates = my_certificates.difference(&their_certificates);
2310        let added_certificates = their_certificates.difference(&my_certificates);
2311
2312        for &(address, fingerprint) in removed_certificates {
2313            v.push(
2314                RequestType::RemoveCertificate(RemoveCertificate {
2315                    address: SocketAddress::from(address),
2316                    fingerprint: fingerprint.to_string(),
2317                })
2318                .into(),
2319            );
2320        }
2321
2322        for &(address, fingerprint) in added_certificates {
2323            if let Some(certificate_and_key) = other
2324                .certificates
2325                .get(&address)
2326                .and_then(|certs| certs.get(fingerprint))
2327            {
2328                v.push(
2329                    RequestType::AddCertificate(AddCertificate {
2330                        address: SocketAddress::from(address),
2331                        certificate: certificate_and_key.clone(),
2332                        expired_at: None,
2333                    })
2334                    .into(),
2335                );
2336            }
2337        }
2338
2339        for address in added_tcp_listeners {
2340            let listener = &other.tcp_listeners[*address];
2341            if listener.active {
2342                v.push(
2343                    RequestType::ActivateListener(ActivateListener {
2344                        address: listener.address,
2345                        proxy: ListenerType::Tcp.into(),
2346                        from_scm: false,
2347                    })
2348                    .into(),
2349                );
2350            }
2351        }
2352
2353        for address in added_udp_listeners {
2354            let listener = &other.udp_listeners[*address];
2355            if listener.active {
2356                v.push(
2357                    RequestType::ActivateListener(ActivateListener {
2358                        address: listener.address,
2359                        proxy: ListenerType::Udp.into(),
2360                        from_scm: false,
2361                    })
2362                    .into(),
2363                );
2364            }
2365        }
2366
2367        // Replay symmetry: the request set `diff` emits, when replayed onto a
2368        // clone of `self`, must reproduce `other`'s routing-relevant maps —
2369        // listeners included, with their `active` flag. This is the property the
2370        // hot-reconfig fan-out relies on — a worker applies these requests and
2371        // must converge on `other`. We verify it in debug by actually replaying;
2372        // `dispatch`'s own invariant sweep runs on every step.
2373        //
2374        // One deliberate normalization: `backends`/`tcp_fronts` are compared
2375        // after dropping empty buckets. `remove_backend`/`remove_tcp_frontend`
2376        // leave an empty `Vec` under a cluster key, whereas `other` may have no
2377        // key at all. An empty bucket emits no requests and is semantically
2378        // equivalent to an absent one, so we normalize it away before comparing.
2379        #[cfg(debug_assertions)]
2380        {
2381            let mut replayed = self.clone();
2382            for request in &v {
2383                // A diff request must always be dispatchable onto `self`.
2384                debug_assert!(
2385                    replayed.dispatch(request).is_ok(),
2386                    "every request emitted by diff must replay cleanly onto self"
2387                );
2388            }
2389            let nonempty = |m: &BTreeMap<ClusterId, Vec<Backend>>| {
2390                m.iter()
2391                    .filter(|(_, v)| !v.is_empty())
2392                    .map(|(k, v)| (k.clone(), v.clone()))
2393                    .collect::<BTreeMap<_, _>>()
2394            };
2395            let nonempty_tcp = |m: &HashMap<ClusterId, Vec<TcpFrontend>>| {
2396                m.iter()
2397                    .filter(|(_, v)| !v.is_empty())
2398                    .map(|(k, v)| (k.clone(), v.clone()))
2399                    .collect::<HashMap<_, _>>()
2400            };
2401            debug_assert!(
2402                replayed.clusters == other.clusters
2403                    && nonempty(&replayed.backends) == nonempty(&other.backends)
2404                    && replayed.http_fronts == other.http_fronts
2405                    && replayed.https_fronts == other.https_fronts
2406                    && nonempty_tcp(&replayed.tcp_fronts) == nonempty_tcp(&other.tcp_fronts)
2407                    && replayed.certificates == other.certificates
2408                    && replayed.http_listeners == other.http_listeners
2409                    && replayed.https_listeners == other.https_listeners
2410                    && replayed.tcp_listeners == other.tcp_listeners
2411                    && replayed.udp_listeners == other.udp_listeners,
2412                "replaying diff(self, other) onto self must reproduce other's clusters/backends/frontends/certificates/listeners"
2413            );
2414        }
2415
2416        v
2417    }
2418
2419    // FIXME: what about deny rules?
2420    pub fn hash_state(&self) -> BTreeMap<ClusterId, u64> {
2421        let mut hm: HashMap<ClusterId, DefaultHasher> = self
2422            .clusters
2423            .keys()
2424            .map(|cluster_id| {
2425                let mut hasher = DefaultHasher::new();
2426                self.clusters.get(cluster_id).hash(&mut hasher);
2427                if let Some(backends) = self.backends.get(cluster_id) {
2428                    backends.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
2429                }
2430                if let Some(tcp_fronts) = self.tcp_fronts.get(cluster_id) {
2431                    tcp_fronts.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
2432                }
2433                (cluster_id.to_owned(), hasher)
2434            })
2435            .collect();
2436
2437        for front in self.http_fronts.values() {
2438            if let Some(cluster_id) = &front.cluster_id
2439                && let Some(hasher) = hm.get_mut(cluster_id)
2440            {
2441                front.hash(hasher);
2442            }
2443        }
2444
2445        for front in self.https_fronts.values() {
2446            if let Some(cluster_id) = &front.cluster_id
2447                && let Some(hasher) = hm.get_mut(cluster_id)
2448            {
2449                front.hash(hasher);
2450            }
2451        }
2452
2453        hm.drain()
2454            .map(|(cluster_id, hasher)| (cluster_id, hasher.finish()))
2455            .collect()
2456    }
2457
2458    /// Gives details about a given cluster.
2459    /// Types like `HttpFrontend` are converted into protobuf ones, like `RequestHttpFrontend`
2460    pub fn cluster_state(&self, cluster_id: &str) -> Option<ClusterInformation> {
2461        let configuration = self.clusters.get(cluster_id).cloned()?;
2462        info!("{:?}", configuration);
2463
2464        let http_frontends: Vec<RequestHttpFrontend> = self
2465            .http_fronts
2466            .values()
2467            .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
2468            .map(|front| front.clone().into())
2469            .collect();
2470
2471        let https_frontends: Vec<RequestHttpFrontend> = self
2472            .https_fronts
2473            .values()
2474            .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
2475            .map(|front| front.clone().into())
2476            .collect();
2477
2478        let tcp_frontends: Vec<RequestTcpFrontend> = self
2479            .tcp_fronts
2480            .get(cluster_id)
2481            .cloned()
2482            .unwrap_or_default()
2483            .iter()
2484            .map(|front| front.clone().into())
2485            .collect();
2486
2487        let udp_frontends: Vec<RequestUdpFrontend> = self
2488            .udp_fronts
2489            .get(cluster_id)
2490            .cloned()
2491            .unwrap_or_default()
2492            .iter()
2493            .map(|front| front.clone().into())
2494            .collect();
2495
2496        let backends: Vec<AddBackend> = self
2497            .backends
2498            .get(cluster_id)
2499            .cloned()
2500            .unwrap_or_default()
2501            .iter()
2502            .map(|backend| backend.clone().into())
2503            .collect();
2504
2505        Some(ClusterInformation {
2506            configuration: Some(configuration),
2507            http_frontends,
2508            https_frontends,
2509            tcp_frontends,
2510            backends,
2511            udp_frontends,
2512        })
2513    }
2514
2515    pub fn count_backends(&self) -> usize {
2516        self.backends.values().fold(0, |acc, v| acc + v.len())
2517    }
2518
2519    pub fn count_frontends(&self) -> usize {
2520        self.http_fronts.values().count()
2521            + self.https_fronts.values().count()
2522            + self.tcp_fronts.values().fold(0, |acc, v| acc + v.len())
2523            + self.udp_fronts.values().fold(0, |acc, v| acc + v.len())
2524    }
2525
2526    pub fn get_cluster_ids_by_domain(
2527        &self,
2528        hostname: String,
2529        path: Option<String>,
2530    ) -> HashSet<ClusterId> {
2531        let mut cluster_ids: HashSet<ClusterId> = HashSet::new();
2532
2533        self.http_fronts.values().for_each(|front| {
2534            if domain_check(&front.hostname, &front.path, &hostname, &path)
2535                && let Some(id) = &front.cluster_id
2536            {
2537                cluster_ids.insert(id.to_string());
2538            }
2539        });
2540
2541        self.https_fronts.values().for_each(|front| {
2542            if domain_check(&front.hostname, &front.path, &hostname, &path)
2543                && let Some(id) = &front.cluster_id
2544            {
2545                cluster_ids.insert(id.to_string());
2546            }
2547        });
2548
2549        cluster_ids
2550    }
2551
2552    pub fn get_certificates(
2553        &self,
2554        filters: QueryCertificatesFilters,
2555    ) -> BTreeMap<String, CertificateAndKey> {
2556        self.certificates
2557            .values()
2558            .flat_map(|hash_map| hash_map.iter())
2559            .filter(|(fingerprint, cert)| {
2560                if let Some(domain) = &filters.domain {
2561                    cert.names.contains(domain)
2562                } else if let Some(f) = &filters.fingerprint {
2563                    fingerprint.to_string() == *f
2564                } else {
2565                    true
2566                }
2567            })
2568            .map(|(fingerprint, cert)| (fingerprint.to_string(), cert.to_owned()))
2569            .collect()
2570    }
2571
2572    pub fn list_frontends(&self, filters: FrontendFilters) -> ListedFrontends {
2573        // if no http / https / tcp filter is provided, list all of them
2574        let list_all = !filters.http && !filters.https && !filters.tcp;
2575
2576        let mut listed_frontends = ListedFrontends::default();
2577
2578        if filters.http || list_all {
2579            for http_frontend in self.http_fronts.iter().filter(|f| {
2580                if let Some(domain) = &filters.domain {
2581                    f.1.hostname.contains(domain)
2582                } else {
2583                    true
2584                }
2585            }) {
2586                listed_frontends
2587                    .http_frontends
2588                    .push(http_frontend.1.to_owned().into());
2589            }
2590        }
2591
2592        if filters.https || list_all {
2593            for https_frontend in self.https_fronts.iter().filter(|f| {
2594                if let Some(domain) = &filters.domain {
2595                    f.1.hostname.contains(domain)
2596                } else {
2597                    true
2598                }
2599            }) {
2600                listed_frontends
2601                    .https_frontends
2602                    .push(https_frontend.1.to_owned().into());
2603            }
2604        }
2605
2606        if (filters.tcp || list_all) && filters.domain.is_none() {
2607            for tcp_frontend in self.tcp_fronts.values().flat_map(|v| v.iter()) {
2608                listed_frontends
2609                    .tcp_frontends
2610                    .push(tcp_frontend.to_owned().into())
2611            }
2612        }
2613
2614        // `FrontendFilters` has no dedicated `udp` flag, so UDP frontends ride
2615        // the same default/all-pass path as TCP: surfaced when no protocol
2616        // filter is set (`list_all`) or when the `tcp` filter is requested.
2617        // Datagram frontends carry no hostname, so a `domain` filter excludes
2618        // them (matching the TCP branch).
2619        if (filters.tcp || list_all) && filters.domain.is_none() {
2620            for udp_frontend in self.udp_fronts.values().flat_map(|v| v.iter()) {
2621                listed_frontends
2622                    .udp_frontends
2623                    .push(udp_frontend.to_owned().into())
2624            }
2625        }
2626
2627        listed_frontends
2628    }
2629
2630    pub fn list_listeners(&self) -> ListenersList {
2631        ListenersList {
2632            http_listeners: self
2633                .http_listeners
2634                .iter()
2635                .map(|(addr, listener)| (addr.to_string(), listener.clone()))
2636                .collect(),
2637            https_listeners: self
2638                .https_listeners
2639                .iter()
2640                .map(|(addr, listener)| (addr.to_string(), listener.clone()))
2641                .collect(),
2642            tcp_listeners: self
2643                .tcp_listeners
2644                .iter()
2645                .map(|(addr, listener)| (addr.to_string(), *listener))
2646                .collect(),
2647            udp_listeners: self
2648                .udp_listeners
2649                .iter()
2650                .map(|(addr, listener)| (addr.to_string(), *listener))
2651                .collect(),
2652        }
2653    }
2654
2655    // create requests needed for a worker to recreate the state
2656    pub fn produce_initial_state(&self) -> InitialState {
2657        let mut worker_requests = Vec::new();
2658        for (counter, request) in self.generate_requests().into_iter().enumerate() {
2659            worker_requests.push(WorkerRequest::new(format!("SAVE-{counter}"), request));
2660        }
2661        InitialState {
2662            requests: worker_requests,
2663        }
2664    }
2665
2666    /// generate requests necessary to recreate the state,
2667    /// in protobuf, to a temp file
2668    pub fn write_initial_state_to_file(&self, file: &mut File) -> Result<usize, StateError> {
2669        let initial_state = self.produce_initial_state();
2670        let count = initial_state.requests.len();
2671
2672        let bytes_to_write = initial_state.encode_to_vec();
2673        println!("writing {} in the temp file", bytes_to_write.len());
2674        file.write_all(&bytes_to_write)
2675            .map_err(StateError::FileError)?;
2676
2677        file.sync_all().map_err(StateError::FileError)?;
2678
2679        Ok(count)
2680    }
2681
2682    /// generate requests necessary to recreate the state,
2683    /// write them in a JSON form in a file, separated by \n\0,
2684    /// returns the number of written requests
2685    pub fn write_requests_to_file(&self, file: &mut File) -> Result<usize, StateError> {
2686        let mut counter = 0usize;
2687        let requests = self.generate_requests();
2688
2689        for request in requests {
2690            let message = WorkerRequest::new(format!("SAVE-{counter}"), request);
2691
2692            file.write_all(
2693                &serde_json::to_string(&message)
2694                    .map(|s| s.into_bytes())
2695                    .unwrap_or_default(),
2696            )
2697            .map_err(StateError::FileError)?;
2698
2699            file.write_all(&b"\n\0"[..])
2700                .map_err(StateError::FileError)?;
2701
2702            if counter.is_multiple_of(1000) {
2703                info!("writing {} commands to file", counter);
2704                file.sync_all().map_err(StateError::FileError)?;
2705            }
2706            counter += 1;
2707        }
2708        file.sync_all().map_err(StateError::FileError)?;
2709
2710        Ok(counter)
2711    }
2712}
2713
2714/// Validate all H2 flood knobs in an HTTP listener patch.
2715///
2716/// Every flood-detector knob (including stream-0 WINDOW_UPDATE) requires a
2717/// value `>= 1`. Passing `0` would disable the detector entirely and leave the
2718/// proxy open to CVE-2023-44487 and related attacks. The runtime constructor
2719/// `H2FloodConfig::new()` applies the same `.max(1)` clamping, but a raw
2720/// protobuf client can bypass the CLI layer, so we enforce the bound here too.
2721///
2722/// `h2_max_concurrent_streams` and `h2_stream_shrink_ratio` are connection-
2723/// config knobs that also require `>= 1`.
2724///
2725/// `h2_graceful_shutdown_deadline_seconds = 0` is intentionally **allowed** —
2726/// it means "wait forever (no forced close after GOAWAY)".
2727pub fn validate_h2_flood_knobs_http(patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
2728    macro_rules! require_ge1 {
2729        ($field:expr, $name:literal) => {
2730            if let Some(0) = $field {
2731                return Err(StateError::InvalidValue {
2732                    field: $name,
2733                    reason: "must be >= 1",
2734                });
2735            }
2736        };
2737    }
2738    require_ge1!(
2739        patch.h2_max_rst_stream_per_window,
2740        "h2_max_rst_stream_per_window"
2741    );
2742    require_ge1!(patch.h2_max_ping_per_window, "h2_max_ping_per_window");
2743    require_ge1!(
2744        patch.h2_max_settings_per_window,
2745        "h2_max_settings_per_window"
2746    );
2747    require_ge1!(
2748        patch.h2_max_empty_data_per_window,
2749        "h2_max_empty_data_per_window"
2750    );
2751    require_ge1!(
2752        patch.h2_max_continuation_frames,
2753        "h2_max_continuation_frames"
2754    );
2755    require_ge1!(patch.h2_max_glitch_count, "h2_max_glitch_count");
2756    require_ge1!(
2757        patch.h2_max_window_update_stream0_per_window,
2758        "h2_max_window_update_stream0_per_window"
2759    );
2760    require_ge1!(patch.h2_max_concurrent_streams, "h2_max_concurrent_streams");
2761    // Shrink ratio runtime floor is 2 (lib/src/protocol/mux/h2.rs ~448 .max(2));
2762    // anything lower is silently promoted so reject at control plane.
2763    if let Some(v) = patch.h2_stream_shrink_ratio
2764        && v < 2
2765    {
2766        return Err(StateError::InvalidValue {
2767            field: "h2_stream_shrink_ratio",
2768            reason: "must be >= 2",
2769        });
2770    }
2771    // Lifetime caps and HPACK limits — must be >= 1 or the runtime trips on the
2772    // first qualifying frame. doc/configure.md advertises "u64 (>= 1)" etc.
2773    require_ge1!(
2774        patch.h2_max_rst_stream_lifetime,
2775        "h2_max_rst_stream_lifetime"
2776    );
2777    require_ge1!(
2778        patch.h2_max_rst_stream_abusive_lifetime,
2779        "h2_max_rst_stream_abusive_lifetime"
2780    );
2781    require_ge1!(
2782        patch.h2_max_rst_stream_emitted_lifetime,
2783        "h2_max_rst_stream_emitted_lifetime"
2784    );
2785    require_ge1!(patch.h2_max_header_list_size, "h2_max_header_list_size");
2786    require_ge1!(patch.h2_max_header_table_size, "h2_max_header_table_size");
2787    require_ge1!(patch.h2_max_header_fields, "h2_max_header_fields");
2788    Ok(())
2789}
2790
2791/// Validate all H2 flood knobs in an HTTPS listener patch (same rules as HTTP).
2792pub fn validate_h2_flood_knobs_https(patch: &UpdateHttpsListenerConfig) -> Result<(), StateError> {
2793    macro_rules! require_ge1 {
2794        ($field:expr, $name:literal) => {
2795            if let Some(0) = $field {
2796                return Err(StateError::InvalidValue {
2797                    field: $name,
2798                    reason: "must be >= 1",
2799                });
2800            }
2801        };
2802    }
2803    require_ge1!(
2804        patch.h2_max_rst_stream_per_window,
2805        "h2_max_rst_stream_per_window"
2806    );
2807    require_ge1!(patch.h2_max_ping_per_window, "h2_max_ping_per_window");
2808    require_ge1!(
2809        patch.h2_max_settings_per_window,
2810        "h2_max_settings_per_window"
2811    );
2812    require_ge1!(
2813        patch.h2_max_empty_data_per_window,
2814        "h2_max_empty_data_per_window"
2815    );
2816    require_ge1!(
2817        patch.h2_max_continuation_frames,
2818        "h2_max_continuation_frames"
2819    );
2820    require_ge1!(patch.h2_max_glitch_count, "h2_max_glitch_count");
2821    require_ge1!(
2822        patch.h2_max_window_update_stream0_per_window,
2823        "h2_max_window_update_stream0_per_window"
2824    );
2825    require_ge1!(patch.h2_max_concurrent_streams, "h2_max_concurrent_streams");
2826    if let Some(v) = patch.h2_stream_shrink_ratio
2827        && v < 2
2828    {
2829        return Err(StateError::InvalidValue {
2830            field: "h2_stream_shrink_ratio",
2831            reason: "must be >= 2",
2832        });
2833    }
2834    require_ge1!(
2835        patch.h2_max_rst_stream_lifetime,
2836        "h2_max_rst_stream_lifetime"
2837    );
2838    require_ge1!(
2839        patch.h2_max_rst_stream_abusive_lifetime,
2840        "h2_max_rst_stream_abusive_lifetime"
2841    );
2842    require_ge1!(
2843        patch.h2_max_rst_stream_emitted_lifetime,
2844        "h2_max_rst_stream_emitted_lifetime"
2845    );
2846    require_ge1!(patch.h2_max_header_list_size, "h2_max_header_list_size");
2847    require_ge1!(patch.h2_max_header_table_size, "h2_max_header_table_size");
2848    require_ge1!(patch.h2_max_header_fields, "h2_max_header_fields");
2849    Ok(())
2850}
2851
2852/// Validate a `sozu_id_header` value against RFC 9110 §5.1 header-name grammar.
2853///
2854/// Rejects empty strings and strings containing CR, LF, colon, space, or tab —
2855/// a conservative approximation of the token grammar that covers all practical
2856/// injection vectors without a full RFC 9110 tokenizer.
2857/// Merge a `CustomHttpAnswers` patch into the listener's stored answers,
2858/// preserving any field not present in the patch.
2859///
2860/// Field-mask semantic: `None` in `patch` means "preserve", `Some` means
2861/// "replace". A no-op patch (all-None) leaves `target` untouched. If `target`
2862/// is currently `None`, initialize it from the patch (any `None` field stays
2863/// `None` so hot-upgrade replay sees the same partial state).
2864pub fn merge_custom_http_answers(
2865    target: &mut Option<CustomHttpAnswers>,
2866    patch: &CustomHttpAnswers,
2867) {
2868    let current = target.get_or_insert_with(CustomHttpAnswers::default);
2869    macro_rules! merge_field {
2870        ($field:ident) => {
2871            if let Some(ref v) = patch.$field {
2872                current.$field = Some(v.clone());
2873            }
2874        };
2875    }
2876    merge_field!(answer_301);
2877    merge_field!(answer_400);
2878    merge_field!(answer_401);
2879    merge_field!(answer_404);
2880    merge_field!(answer_408);
2881    merge_field!(answer_413);
2882    merge_field!(answer_421);
2883    merge_field!(answer_502);
2884    merge_field!(answer_503);
2885    merge_field!(answer_504);
2886    merge_field!(answer_507);
2887}
2888
2889/// Validate an `AlpnProtocols` patch: each value must be "h2" or "http/1.1".
2890/// Empty values vec is allowed (reset-to-default).
2891pub fn validate_alpn_protocols(values: &[String]) -> Result<(), StateError> {
2892    for value in values {
2893        if value != "h2" && value != "http/1.1" {
2894            return Err(StateError::InvalidValue {
2895                field: "alpn_protocols",
2896                reason: "each value must be \"h2\" or \"http/1.1\"",
2897            });
2898        }
2899    }
2900    Ok(())
2901}
2902
2903/// Validate a `sozu_id_header` value against the RFC 9110 §5.1 `token` grammar:
2904///
2905/// ```text
2906/// token  = 1*tchar
2907/// tchar  = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
2908///          "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
2909/// ```
2910///
2911/// Rejects empty strings, non-ASCII bytes, controls (including CR/LF/tab),
2912/// separators (including colon and space), and any other non-`tchar` byte.
2913pub fn validate_sozu_id_header(value: &str) -> Result<(), StateError> {
2914    if value.is_empty() {
2915        return Err(StateError::InvalidValue {
2916            field: "sozu_id_header",
2917            reason: "must not be empty",
2918        });
2919    }
2920    for b in value.bytes() {
2921        let is_tchar = b.is_ascii_alphanumeric()
2922            || matches!(
2923                b,
2924                b'!' | b'#'
2925                    | b'$'
2926                    | b'%'
2927                    | b'&'
2928                    | b'\''
2929                    | b'*'
2930                    | b'+'
2931                    | b'-'
2932                    | b'.'
2933                    | b'^'
2934                    | b'_'
2935                    | b'`'
2936                    | b'|'
2937                    | b'~'
2938            );
2939        if !is_tchar {
2940            return Err(StateError::InvalidValue {
2941                field: "sozu_id_header",
2942                reason: "must be a valid HTTP header name (RFC 9110 §5.1 token: alphanumeric or one of !#$%&'*+-.^_`|~)",
2943            });
2944        }
2945    }
2946    Ok(())
2947}
2948
2949fn domain_check(
2950    front_hostname: &str,
2951    front_path_rule: &PathRule,
2952    hostname: &str,
2953    path_prefix: &Option<String>,
2954) -> bool {
2955    if hostname != front_hostname {
2956        return false;
2957    }
2958
2959    if let Some(path) = &path_prefix {
2960        return path == &front_path_rule.value;
2961    }
2962
2963    true
2964}
2965
2966/// Canonicalize a TCP frontend's ALPN list into its sorted, deduplicated set
2967/// representation. ALPN matching (the worker's `AlpnMatcher::OneOf` in
2968/// `lib/src/tcp.rs`) is semantically a *set* of protocol names, not an
2969/// ordered list: comparing `Vec`s directly treats `["h2", "http/1.1"]` and
2970/// `["http/1.1", "h2"]` as distinct identities, silently admitting a
2971/// duplicate/overlapping frontend the worker rejects on the next fan-out
2972/// (sozu-proxy/sozu#1290). Used both to canonicalize a frontend for storage
2973/// and to canonicalize a candidate for comparison, so `TcpFrontend.alpn` is
2974/// always already in this form.
2975fn canonical_tcp_alpn(alpn: &[String]) -> Vec<String> {
2976    let mut alpn = alpn.to_vec();
2977    alpn.sort();
2978    alpn.dedup();
2979    // POST: strictly increasing (sorted, no adjacent duplicates) — the
2980    // single property every caller relies on to treat two canonical lists
2981    // as set-equal via plain `==`.
2982    debug_assert!(
2983        alpn.windows(2).all(|pair| pair[0] < pair[1]),
2984        "canonical_tcp_alpn must return a strictly sorted, duplicate-free list"
2985    );
2986    alpn
2987}
2988
2989/// Identity predicate for a TCP frontend: `(address, sni, alpn)`, not the
2990/// whole struct -- two frontends differing only in `tags` still collide
2991/// here even though they compare unequal as full structs, since they'd
2992/// still match the exact same wire traffic. `sni` and `alpn` are expected
2993/// to already be in canonical form (normalized-lowercase sni, sorted+deduped
2994/// alpn via [`canonical_tcp_alpn`]) on both sides -- this is a plain
2995/// equality check, not itself a canonicalizer. Shared by
2996/// `add_tcp_frontend`'s duplicate check and `remove_tcp_frontend`'s
2997/// retain/postcondition so the three call sites can never drift apart
2998/// (sozu-proxy/sozu#1290).
2999fn tcp_frontend_matches(
3000    front: &TcpFrontend,
3001    address: SocketAddr,
3002    sni: &Option<String>,
3003    alpn: &[String],
3004) -> bool {
3005    front.address == address && front.sni == *sni && front.alpn.as_slice() == alpn
3006}
3007
3008struct DiffMap<'a, K: Ord, V, I1, I2> {
3009    my_it: I1,
3010    other_it: I2,
3011    my: Option<(K, &'a V)>,
3012    other: Option<(K, &'a V)>,
3013}
3014
3015//fn diff_map<'a, K:Ord, V: PartialEq>(my: &'a BTreeMap<K,V>, other: &'a BTreeMap<K,V>) -> DiffMap<'a,K,V> {
3016fn diff_map<
3017    'a,
3018    K: Ord,
3019    V: PartialEq,
3020    I1: Iterator<Item = (K, &'a V)>,
3021    I2: Iterator<Item = (K, &'a V)>,
3022>(
3023    my: I1,
3024    other: I2,
3025) -> DiffMap<'a, K, V, I1, I2> {
3026    DiffMap {
3027        my_it: my,
3028        other_it: other,
3029        my: None,
3030        other: None,
3031    }
3032}
3033
3034enum DiffResult {
3035    Added,
3036    Removed,
3037    Changed,
3038}
3039
3040// this will iterate over the keys of both iterators
3041// since keys are sorted, it should be easy to see which ones are in common or not
3042impl<'a, K: Ord, V: PartialEq, I1: Iterator<Item = (K, &'a V)>, I2: Iterator<Item = (K, &'a V)>>
3043    std::iter::Iterator for DiffMap<'a, K, V, I1, I2>
3044{
3045    type Item = (K, DiffResult);
3046
3047    fn next(&mut self) -> Option<Self::Item> {
3048        loop {
3049            if self.my.is_none() {
3050                self.my = self.my_it.next();
3051            }
3052            if self.other.is_none() {
3053                self.other = self.other_it.next();
3054            }
3055
3056            match (self.my.take(), self.other.take()) {
3057                // there are no more elements in my_it, all the next elements in other
3058                // should be added
3059                // if other was none, we will stop the iterator there
3060                (None, other) => return other.map(|(k, _)| (k, DiffResult::Added)),
3061                // there are no more elements in other_it, all the next elements in my
3062                // should be removed
3063                (Some((k, _)), None) => return Some((k, DiffResult::Removed)),
3064                // element is present in my but not other
3065                (Some((k1, _v1)), Some((k2, v2))) if k1 < k2 => {
3066                    self.other = Some((k2, v2));
3067                    return Some((k1, DiffResult::Removed));
3068                }
3069                // element is present in other byt not in my
3070                (Some((k1, v1)), Some((k2, _v2))) if k1 > k2 => {
3071                    self.my = Some((k1, v1));
3072                    return Some((k2, DiffResult::Added));
3073                }
3074                (Some((k1, v1)), Some((_k2, v2))) if v1 != v2 => {
3075                    // key is present in both, if elements have changed
3076                    // return a value, otherwise go to the next key for both maps
3077                    return Some((k1, DiffResult::Changed));
3078                }
3079                _ => {}
3080            }
3081        }
3082    }
3083}
3084
3085#[cfg(test)]
3086mod tests {
3087    use rand::{RngExt, rng, seq::SliceRandom};
3088
3089    use super::*;
3090    use crate::proto::command::{
3091        CustomHttpAnswers, LoadBalancingParams, RequestHttpFrontend, RequestTcpFrontend,
3092        RequestUdpFrontend, RulePosition, UdpListenerConfig, UpdateUdpListenerConfig,
3093    };
3094
3095    #[test]
3096    fn serialize() {
3097        let mut state: ConfigState = Default::default();
3098        state
3099            .dispatch(
3100                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3101                    cluster_id: Some(String::from("cluster_1")),
3102                    hostname: String::from("lolcatho.st:8080"),
3103                    path: PathRule::prefix(String::from("/")),
3104                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3105                    position: RulePosition::Tree.into(),
3106                    ..Default::default()
3107                })
3108                .into(),
3109            )
3110            .expect("Could not execute request");
3111        state
3112            .dispatch(
3113                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3114                    cluster_id: Some(String::from("cluster_2")),
3115                    hostname: String::from("test.local"),
3116                    path: PathRule::prefix(String::from("/abc")),
3117                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3118                    position: RulePosition::Pre.into(),
3119                    ..Default::default()
3120                })
3121                .into(),
3122            )
3123            .expect("Could not execute request");
3124        state
3125            .dispatch(
3126                &RequestType::AddBackend(AddBackend {
3127                    cluster_id: String::from("cluster_1"),
3128                    backend_id: String::from("cluster_1-0"),
3129                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3130                    ..Default::default()
3131                })
3132                .into(),
3133            )
3134            .expect("Could not execute request");
3135        state
3136            .dispatch(
3137                &RequestType::AddBackend(AddBackend {
3138                    cluster_id: String::from("cluster_1"),
3139                    backend_id: String::from("cluster_1-1"),
3140                    address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
3141                    ..Default::default()
3142                })
3143                .into(),
3144            )
3145            .expect("Could not execute request");
3146        state
3147            .dispatch(
3148                &RequestType::AddBackend(AddBackend {
3149                    cluster_id: String::from("cluster_2"),
3150                    backend_id: String::from("cluster_2-0"),
3151                    address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
3152                    ..Default::default()
3153                })
3154                .into(),
3155            )
3156            .expect("Could not execute request");
3157        state
3158            .dispatch(
3159                &RequestType::AddBackend(AddBackend {
3160                    cluster_id: String::from("cluster_1"),
3161                    backend_id: String::from("cluster_1-3"),
3162                    address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
3163                    ..Default::default()
3164                })
3165                .into(),
3166            )
3167            .expect("Could not execute request");
3168        state
3169            .dispatch(
3170                &RequestType::RemoveBackend(RemoveBackend {
3171                    cluster_id: String::from("cluster_1"),
3172                    backend_id: String::from("cluster_1-3"),
3173                    address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
3174                })
3175                .into(),
3176            )
3177            .expect("Could not execute request");
3178
3179        /*
3180        let encoded = state.encode();
3181        println!("serialized:\n{}", encoded);
3182
3183        let new_state: Option<HttpProxy> = decode_str(&encoded);
3184        println!("deserialized:\n{:?}", new_state);
3185        assert_eq!(new_state, Some(state));
3186        */
3187        //assert!(false);
3188    }
3189
3190    #[test]
3191    fn diff() {
3192        let mut state: ConfigState = Default::default();
3193        state
3194            .dispatch(
3195                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3196                    cluster_id: Some(String::from("cluster_1")),
3197                    hostname: String::from("lolcatho.st:8080"),
3198                    path: PathRule::prefix(String::from("/")),
3199                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3200                    position: RulePosition::Post.into(),
3201                    ..Default::default()
3202                })
3203                .into(),
3204            )
3205            .expect("Could not execute request");
3206        state
3207            .dispatch(
3208                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3209                    cluster_id: Some(String::from("cluster_2")),
3210                    hostname: String::from("test.local"),
3211                    path: PathRule::prefix(String::from("/abc")),
3212                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3213                    ..Default::default()
3214                })
3215                .into(),
3216            )
3217            .expect("Could not execute request");
3218        state
3219            .dispatch(
3220                &RequestType::AddBackend(AddBackend {
3221                    cluster_id: String::from("cluster_1"),
3222                    backend_id: String::from("cluster_1-0"),
3223                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3224                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3225                    ..Default::default()
3226                })
3227                .into(),
3228            )
3229            .expect("Could not execute request");
3230        state
3231            .dispatch(
3232                &RequestType::AddBackend(AddBackend {
3233                    cluster_id: String::from("cluster_1"),
3234                    backend_id: String::from("cluster_1-1"),
3235                    address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
3236                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3237                    ..Default::default()
3238                })
3239                .into(),
3240            )
3241            .expect("Could not execute request");
3242        state
3243            .dispatch(
3244                &RequestType::AddBackend(AddBackend {
3245                    cluster_id: String::from("cluster_2"),
3246                    backend_id: String::from("cluster_2-0"),
3247                    address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
3248                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3249                    ..Default::default()
3250                })
3251                .into(),
3252            )
3253            .expect("Could not execute request");
3254        state
3255            .dispatch(
3256                &RequestType::AddCluster(Cluster {
3257                    cluster_id: String::from("cluster_2"),
3258                    sticky_session: true,
3259                    https_redirect: true,
3260                    ..Default::default()
3261                })
3262                .into(),
3263            )
3264            .expect("Could not execute request");
3265
3266        let mut state2: ConfigState = Default::default();
3267        state2
3268            .dispatch(
3269                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3270                    cluster_id: Some(String::from("cluster_1")),
3271                    hostname: String::from("lolcatho.st:8080"),
3272                    path: PathRule::prefix(String::from("/")),
3273                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3274                    position: RulePosition::Post.into(),
3275                    ..Default::default()
3276                })
3277                .into(),
3278            )
3279            .expect("Could not execute request");
3280        state2
3281            .dispatch(
3282                &RequestType::AddBackend(AddBackend {
3283                    cluster_id: String::from("cluster_1"),
3284                    backend_id: String::from("cluster_1-0"),
3285                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3286                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3287                    ..Default::default()
3288                })
3289                .into(),
3290            )
3291            .expect("Could not execute request");
3292        state2
3293            .dispatch(
3294                &RequestType::AddBackend(AddBackend {
3295                    cluster_id: String::from("cluster_1"),
3296                    backend_id: String::from("cluster_1-1"),
3297                    address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
3298                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3299                    ..Default::default()
3300                })
3301                .into(),
3302            )
3303            .expect("Could not execute request");
3304        state2
3305            .dispatch(
3306                &RequestType::AddBackend(AddBackend {
3307                    cluster_id: String::from("cluster_1"),
3308                    backend_id: String::from("cluster_1-2"),
3309                    address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
3310                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3311                    ..Default::default()
3312                })
3313                .into(),
3314            )
3315            .expect("Could not execute request");
3316        state2
3317            .dispatch(
3318                &RequestType::AddCluster(Cluster {
3319                    cluster_id: String::from("cluster_3"),
3320                    sticky_session: false,
3321                    https_redirect: false,
3322                    ..Default::default()
3323                })
3324                .into(),
3325            )
3326            .expect("Could not execute request");
3327
3328        let e: Vec<Request> = vec![
3329            RequestType::RemoveHttpFrontend(RequestHttpFrontend {
3330                cluster_id: Some(String::from("cluster_2")),
3331                hostname: String::from("test.local"),
3332                path: PathRule::prefix(String::from("/abc")),
3333                address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3334                ..Default::default()
3335            })
3336            .into(),
3337            RequestType::RemoveBackend(RemoveBackend {
3338                cluster_id: String::from("cluster_2"),
3339                backend_id: String::from("cluster_2-0"),
3340                address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
3341            })
3342            .into(),
3343            RequestType::AddBackend(AddBackend {
3344                cluster_id: String::from("cluster_1"),
3345                backend_id: String::from("cluster_1-2"),
3346                address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
3347                load_balancing_parameters: Some(LoadBalancingParams::default()),
3348                ..Default::default()
3349            })
3350            .into(),
3351            RequestType::RemoveCluster(String::from("cluster_2")).into(),
3352            RequestType::AddCluster(Cluster {
3353                cluster_id: String::from("cluster_3"),
3354                sticky_session: false,
3355                https_redirect: false,
3356                ..Default::default()
3357            })
3358            .into(),
3359        ];
3360        let expected_diff: HashSet<&Request> = HashSet::from_iter(e.iter());
3361
3362        let d = state.diff(&state2);
3363        let diff = HashSet::from_iter(d.iter());
3364        println!("diff requests:\n{diff:#?}\n");
3365        println!("expected diff requests:\n{expected_diff:#?}\n");
3366
3367        let hash1 = state.hash_state();
3368        let hash2 = state2.hash_state();
3369        let mut state3 = state.clone();
3370        state3
3371            .dispatch(
3372                &RequestType::AddBackend(AddBackend {
3373                    cluster_id: String::from("cluster_1"),
3374                    backend_id: String::from("cluster_1-2"),
3375                    address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
3376                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3377                    ..Default::default()
3378                })
3379                .into(),
3380            )
3381            .expect("Could not execute request");
3382        let hash3 = state3.hash_state();
3383        println!("state 1 hashes: {hash1:#?}");
3384        println!("state 2 hashes: {hash2:#?}");
3385        println!("state 3 hashes: {hash3:#?}");
3386
3387        assert_eq!(diff, expected_diff);
3388    }
3389
3390    #[test]
3391    fn cluster_ids_by_domain() {
3392        let mut config = ConfigState::new();
3393        let http_front_cluster1 = RequestHttpFrontend {
3394            cluster_id: Some(String::from("MyCluster_1")),
3395            hostname: String::from("lolcatho.st"),
3396            path: PathRule::prefix(String::from("")),
3397            address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3398            ..Default::default()
3399        };
3400
3401        let https_front_cluster1 = RequestHttpFrontend {
3402            cluster_id: Some(String::from("MyCluster_1")),
3403            hostname: String::from("lolcatho.st"),
3404            path: PathRule::prefix(String::from("")),
3405            address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3406            ..Default::default()
3407        };
3408
3409        let http_front_cluster2 = RequestHttpFrontend {
3410            cluster_id: Some(String::from("MyCluster_2")),
3411            hostname: String::from("lolcatho.st"),
3412            path: PathRule::prefix(String::from("/api")),
3413            address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3414            ..Default::default()
3415        };
3416
3417        let https_front_cluster2 = RequestHttpFrontend {
3418            cluster_id: Some(String::from("MyCluster_2")),
3419            hostname: String::from("lolcatho.st"),
3420            path: PathRule::prefix(String::from("/api")),
3421            address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3422            ..Default::default()
3423        };
3424
3425        config
3426            .dispatch(&RequestType::AddHttpFrontend(http_front_cluster1).into())
3427            .expect("Could not execute request");
3428        config
3429            .dispatch(&RequestType::AddHttpFrontend(http_front_cluster2).into())
3430            .expect("Could not execute request");
3431        config
3432            .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster1).into())
3433            .expect("Could not execute request");
3434        config
3435            .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster2).into())
3436            .expect("Could not execute request");
3437
3438        let mut cluster1_cluster2: HashSet<ClusterId> = HashSet::new();
3439        cluster1_cluster2.insert(String::from("MyCluster_1"));
3440        cluster1_cluster2.insert(String::from("MyCluster_2"));
3441
3442        let mut cluster2: HashSet<ClusterId> = HashSet::new();
3443        cluster2.insert(String::from("MyCluster_2"));
3444
3445        let empty: HashSet<ClusterId> = HashSet::new();
3446        assert_eq!(
3447            config.get_cluster_ids_by_domain(String::from("lolcatho.st"), None),
3448            cluster1_cluster2
3449        );
3450        assert_eq!(
3451            config
3452                .get_cluster_ids_by_domain(String::from("lolcatho.st"), Some(String::from("/api"))),
3453            cluster2
3454        );
3455        assert_eq!(
3456            config.get_cluster_ids_by_domain(String::from("lolcathost"), None),
3457            empty
3458        );
3459        assert_eq!(
3460            config
3461                .get_cluster_ids_by_domain(String::from("lolcathost"), Some(String::from("/sozu"))),
3462            empty
3463        );
3464    }
3465
3466    #[test]
3467    fn duplicate_backends() {
3468        let mut state: ConfigState = Default::default();
3469        state
3470            .dispatch(
3471                &RequestType::AddBackend(AddBackend {
3472                    cluster_id: String::from("cluster_1"),
3473                    backend_id: String::from("cluster_1-0"),
3474                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3475                    load_balancing_parameters: Some(LoadBalancingParams::default()),
3476                    ..Default::default()
3477                })
3478                .into(),
3479            )
3480            .expect("Could not execute request");
3481
3482        let b = Backend {
3483            cluster_id: String::from("cluster_1"),
3484            backend_id: String::from("cluster_1-0"),
3485            address: "127.0.0.1:1026".parse().unwrap(),
3486            load_balancing_parameters: Some(LoadBalancingParams::default()),
3487            sticky_id: Some("sticky".to_string()),
3488            backup: None,
3489        };
3490
3491        state
3492            .dispatch(&RequestType::AddBackend(b.clone().to_add_backend()).into())
3493            .expect("Could not execute order");
3494
3495        assert_eq!(state.backends.get("cluster_1").unwrap(), &vec![b]);
3496    }
3497
3498    #[test]
3499    fn remove_backend() {
3500        let mut state: ConfigState = Default::default();
3501        state
3502            .dispatch(
3503                &RequestType::AddCluster(Cluster {
3504                    cluster_id: String::from("cluster_1"),
3505                    ..Default::default()
3506                })
3507                .into(),
3508            )
3509            .expect("Could not execute request");
3510
3511        for i in 0..10 {
3512            state
3513                .dispatch(
3514                    &RequestType::AddBackend(AddBackend {
3515                        cluster_id: String::from("cluster_1"),
3516                        backend_id: format!("cluster_1-{i}"),
3517                        address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3518                        ..Default::default()
3519                    })
3520                    .into(),
3521                )
3522                .expect("Could not execute request");
3523        }
3524
3525        assert_eq!(state.backends.get("cluster_1").unwrap().len(), 10);
3526
3527        let remove_backend_2 = RequestType::RemoveBackend(RemoveBackend {
3528            cluster_id: String::from("cluster_1"),
3529            backend_id: String::from("cluster_1-0"),
3530            address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3531        })
3532        .into();
3533
3534        let remove_backend_result = state.dispatch(&remove_backend_2);
3535
3536        assert!(remove_backend_result.is_ok());
3537        assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
3538
3539        let redundant_remove = state.dispatch(&remove_backend_2);
3540        assert!(matches!(redundant_remove, Err(StateError::NoChange)));
3541        assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
3542    }
3543
3544    #[test]
3545    fn remove_backends_randomly() {
3546        let mut state: ConfigState = Default::default();
3547        state
3548            .dispatch(
3549                &RequestType::AddCluster(Cluster {
3550                    cluster_id: String::from("cluster_1"),
3551                    ..Default::default()
3552                })
3553                .into(),
3554            )
3555            .expect("Could not execute request");
3556
3557        for _ in 0..1000 {
3558            for i in 0..10 {
3559                state
3560                    .dispatch(
3561                        &RequestType::AddBackend(AddBackend {
3562                            cluster_id: String::from("cluster_1"),
3563                            backend_id: format!("cluster_1-{i}"),
3564                            address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3565                            ..Default::default()
3566                        })
3567                        .into(),
3568                    )
3569                    .expect("Could not execute request");
3570            }
3571
3572            let mut rng = rng();
3573            let mut indexes = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
3574            indexes.shuffle(&mut rng);
3575            let random_count = rng.random_range(1..indexes.len());
3576            let random_indexes: Vec<i32> = indexes.into_iter().take(random_count).collect();
3577
3578            for j in random_indexes {
3579                let remove_backend_result = state.dispatch(
3580                    &RequestType::RemoveBackend(RemoveBackend {
3581                        cluster_id: String::from("cluster_1"),
3582                        backend_id: format!("cluster_1-{j}"),
3583                        address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3584                    })
3585                    .into(),
3586                );
3587                assert!(remove_backend_result.is_ok());
3588            }
3589        }
3590    }
3591
3592    #[test]
3593    fn listener_diff() {
3594        let mut state: ConfigState = Default::default();
3595        let custom_http_answers = Some(CustomHttpAnswers {
3596            answer_404: Some("test".to_string()),
3597            ..Default::default()
3598        });
3599        state
3600            .dispatch(
3601                &RequestType::AddTcpListener(TcpListenerConfig {
3602                    address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3603                    ..Default::default()
3604                })
3605                .into(),
3606            )
3607            .expect("Could not execute request");
3608        state
3609            .dispatch(
3610                &RequestType::ActivateListener(ActivateListener {
3611                    address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3612                    proxy: ListenerType::Tcp.into(),
3613                    from_scm: false,
3614                })
3615                .into(),
3616            )
3617            .expect("Could not execute request");
3618        state
3619            .dispatch(
3620                &RequestType::AddHttpListener(HttpListenerConfig {
3621                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3622                    ..Default::default()
3623                })
3624                .into(),
3625            )
3626            .expect("Could not execute request");
3627        state
3628            .dispatch(
3629                &RequestType::AddHttpsListener(HttpsListenerConfig {
3630                    address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3631                    ..Default::default()
3632                })
3633                .into(),
3634            )
3635            .expect("Could not execute request");
3636        state
3637            .dispatch(
3638                &RequestType::ActivateListener(ActivateListener {
3639                    address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3640                    proxy: ListenerType::Https.into(),
3641                    from_scm: false,
3642                })
3643                .into(),
3644            )
3645            .expect("Could not execute request");
3646
3647        let mut state2: ConfigState = Default::default();
3648        state2
3649            .dispatch(
3650                &RequestType::AddTcpListener(TcpListenerConfig {
3651                    address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3652                    expect_proxy: true,
3653                    ..Default::default()
3654                })
3655                .into(),
3656            )
3657            .expect("Could not execute request");
3658        state2
3659            .dispatch(
3660                &RequestType::AddHttpListener(HttpListenerConfig {
3661                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3662                    http_answers: custom_http_answers.clone(),
3663                    ..Default::default()
3664                })
3665                .into(),
3666            )
3667            .expect("Could not execute request");
3668        state2
3669            .dispatch(
3670                &RequestType::ActivateListener(ActivateListener {
3671                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3672                    proxy: ListenerType::Http.into(),
3673                    from_scm: false,
3674                })
3675                .into(),
3676            )
3677            .expect("Could not execute request");
3678        state2
3679            .dispatch(
3680                &RequestType::AddHttpsListener(HttpsListenerConfig {
3681                    address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3682                    http_answers: custom_http_answers.clone(),
3683                    ..Default::default()
3684                })
3685                .into(),
3686            )
3687            .expect("Could not execute request");
3688        state2
3689            .dispatch(
3690                &RequestType::ActivateListener(ActivateListener {
3691                    address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3692                    proxy: ListenerType::Https.into(),
3693                    from_scm: false,
3694                })
3695                .into(),
3696            )
3697            .expect("Could not execute request");
3698
3699        let e: Vec<Request> = vec![
3700            RequestType::RemoveListener(RemoveListener {
3701                address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3702                proxy: ListenerType::Tcp.into(),
3703            })
3704            .into(),
3705            RequestType::AddTcpListener(TcpListenerConfig {
3706                address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3707                expect_proxy: true,
3708                ..Default::default()
3709            })
3710            .into(),
3711            RequestType::DeactivateListener(DeactivateListener {
3712                address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
3713                proxy: ListenerType::Tcp.into(),
3714                to_scm: false,
3715            })
3716            .into(),
3717            RequestType::RemoveListener(RemoveListener {
3718                address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3719                proxy: ListenerType::Http.into(),
3720            })
3721            .into(),
3722            RequestType::AddHttpListener(HttpListenerConfig {
3723                address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3724                http_answers: custom_http_answers.clone(),
3725                ..Default::default()
3726            })
3727            .into(),
3728            RequestType::ActivateListener(ActivateListener {
3729                address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3730                proxy: ListenerType::Http.into(),
3731                from_scm: false,
3732            })
3733            .into(),
3734            RequestType::RemoveListener(RemoveListener {
3735                address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3736                proxy: ListenerType::Https.into(),
3737            })
3738            .into(),
3739            RequestType::AddHttpsListener(HttpsListenerConfig {
3740                address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3741                http_answers: custom_http_answers.clone(),
3742                ..Default::default()
3743            })
3744            .into(),
3745            // The 8443 HTTPS listener is active in both states but its content
3746            // changed (custom answers). The Remove + Add(active=false) above
3747            // wipes the active flag, so diff must re-emit an ActivateListener to
3748            // keep the listener live across the hot reconfig. Without it the
3749            // worker would silently deactivate the listener on replay.
3750            RequestType::ActivateListener(ActivateListener {
3751                address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3752                proxy: ListenerType::Https.into(),
3753                from_scm: false,
3754            })
3755            .into(),
3756        ];
3757
3758        let diff = state.diff(&state2);
3759        //let diff: HashSet<&RequestContent> = HashSet::from_iter(d.iter());
3760        println!("expected diff requests:\n{e:#?}\n");
3761        println!("diff requests:\n{diff:#?}\n");
3762
3763        let _hash1 = state.hash_state();
3764        let _hash2 = state2.hash_state();
3765
3766        assert_eq!(diff, e);
3767
3768        // Round-trip: replaying diff(state -> state2) onto a clone of `state`
3769        // must reproduce `state2`'s listener maps EXACTLY, active flag included.
3770        // This is the hot-reconfig correctness property — a worker applies these
3771        // requests and must converge on the target state. In particular the 8443
3772        // HTTPS listener (active in both states, content changed) must come back
3773        // ACTIVE, which is the bug the ActivateListener re-emission above fixes.
3774        let mut replayed = state.clone();
3775        for request in &diff {
3776            replayed
3777                .dispatch(request)
3778                .expect("every diff request must replay cleanly onto the source state");
3779        }
3780        assert_eq!(
3781            replayed.tcp_listeners, state2.tcp_listeners,
3782            "replayed tcp_listeners must match the target state"
3783        );
3784        assert_eq!(
3785            replayed.http_listeners, state2.http_listeners,
3786            "replayed http_listeners must match the target state"
3787        );
3788        assert_eq!(
3789            replayed.https_listeners, state2.https_listeners,
3790            "replayed https_listeners must match the target state"
3791        );
3792        // Explicitly assert the still-active listener stays active across the
3793        // config change (the core of the fixed bug).
3794        let replayed_8443 = replayed
3795            .https_listeners
3796            .get(&SocketAddr::from(SocketAddress::new_v4(0, 0, 0, 0, 8443)))
3797            .expect("8443 HTTPS listener must exist after replay");
3798        assert!(
3799            replayed_8443.active,
3800            "the 8443 HTTPS listener must stay ACTIVE across a config change"
3801        );
3802    }
3803
3804    #[test]
3805    fn certificate_retrieval() {
3806        let mut state: ConfigState = Default::default();
3807        let certificate_and_key = CertificateAndKey {
3808            certificate: String::from(include_str!("../assets/certificate.pem")),
3809            key: String::from(include_str!("../assets/key.pem")),
3810            certificate_chain: vec![],
3811            versions: vec![],
3812            names: vec!["lolcatho.st".to_string()],
3813        };
3814        let add_certificate = AddCertificate {
3815            address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
3816            certificate: certificate_and_key,
3817            expired_at: None,
3818        };
3819        state
3820            .dispatch(&RequestType::AddCertificate(add_certificate).into())
3821            .expect("Could not add certificate");
3822
3823        println!("state: {state:#?}");
3824
3825        // let fingerprint: Fingerprint = serde_json::from_str(
3826        //     "\"ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5\"",
3827        // )
3828        // .expect("Could not deserialize the fingerprint");
3829
3830        let certificates_found_by_fingerprint = state.get_certificates(QueryCertificatesFilters {
3831            domain: None,
3832            fingerprint: Some(
3833                "ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5".to_string(),
3834            ),
3835        });
3836
3837        println!("found certificate: {certificates_found_by_fingerprint:#?}");
3838
3839        assert!(!certificates_found_by_fingerprint.is_empty());
3840
3841        let certificate_found_by_domain_name = state.get_certificates(QueryCertificatesFilters {
3842            domain: Some("lolcatho.st".to_string()),
3843            fingerprint: None,
3844        });
3845
3846        assert!(!certificate_found_by_domain_name.is_empty());
3847    }
3848
3849    #[test]
3850    fn count_backends_across_clusters() {
3851        let mut state: ConfigState = Default::default();
3852
3853        assert_eq!(state.count_backends(), 0);
3854
3855        state
3856            .dispatch(
3857                &RequestType::AddBackend(AddBackend {
3858                    cluster_id: String::from("cluster_1"),
3859                    backend_id: String::from("cluster_1-0"),
3860                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3861                    ..Default::default()
3862                })
3863                .into(),
3864            )
3865            .expect("Could not execute request");
3866        assert_eq!(state.count_backends(), 1);
3867
3868        state
3869            .dispatch(
3870                &RequestType::AddBackend(AddBackend {
3871                    cluster_id: String::from("cluster_1"),
3872                    backend_id: String::from("cluster_1-1"),
3873                    address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
3874                    ..Default::default()
3875                })
3876                .into(),
3877            )
3878            .expect("Could not execute request");
3879        assert_eq!(state.count_backends(), 2);
3880
3881        // add backend to a second cluster
3882        state
3883            .dispatch(
3884                &RequestType::AddBackend(AddBackend {
3885                    cluster_id: String::from("cluster_2"),
3886                    backend_id: String::from("cluster_2-0"),
3887                    address: SocketAddress::new_v4(192, 168, 1, 1, 8080),
3888                    ..Default::default()
3889                })
3890                .into(),
3891            )
3892            .expect("Could not execute request");
3893        assert_eq!(state.count_backends(), 3);
3894
3895        // remove a backend and verify count decreases
3896        state
3897            .dispatch(
3898                &RequestType::RemoveBackend(RemoveBackend {
3899                    cluster_id: String::from("cluster_1"),
3900                    backend_id: String::from("cluster_1-0"),
3901                    address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
3902                })
3903                .into(),
3904            )
3905            .expect("Could not execute request");
3906        assert_eq!(state.count_backends(), 2);
3907    }
3908
3909    #[test]
3910    fn count_frontends_across_types() {
3911        let mut state: ConfigState = Default::default();
3912
3913        assert_eq!(state.count_frontends(), 0);
3914
3915        // add an HTTP frontend
3916        state
3917            .dispatch(
3918                &RequestType::AddHttpFrontend(RequestHttpFrontend {
3919                    cluster_id: Some(String::from("cluster_1")),
3920                    hostname: String::from("example.com"),
3921                    path: PathRule::prefix(String::from("/")),
3922                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3923                    position: RulePosition::Tree.into(),
3924                    ..Default::default()
3925                })
3926                .into(),
3927            )
3928            .expect("Could not execute request");
3929        assert_eq!(state.count_frontends(), 1);
3930
3931        // add an HTTPS frontend
3932        state
3933            .dispatch(
3934                &RequestType::AddHttpsFrontend(RequestHttpFrontend {
3935                    cluster_id: Some(String::from("cluster_1")),
3936                    hostname: String::from("secure.example.com"),
3937                    path: PathRule::prefix(String::from("/")),
3938                    address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
3939                    position: RulePosition::Tree.into(),
3940                    ..Default::default()
3941                })
3942                .into(),
3943            )
3944            .expect("Could not execute request");
3945        assert_eq!(state.count_frontends(), 2);
3946
3947        // add a TCP frontend
3948        state
3949            .dispatch(
3950                &RequestType::AddTcpFrontend(RequestTcpFrontend {
3951                    cluster_id: String::from("cluster_2"),
3952                    address: SocketAddress::new_v4(0, 0, 0, 0, 5432),
3953                    ..Default::default()
3954                })
3955                .into(),
3956            )
3957            .expect("Could not execute request");
3958        assert_eq!(state.count_frontends(), 3);
3959
3960        // add a second TCP frontend on the same cluster
3961        state
3962            .dispatch(
3963                &RequestType::AddTcpFrontend(RequestTcpFrontend {
3964                    cluster_id: String::from("cluster_2"),
3965                    address: SocketAddress::new_v4(0, 0, 0, 0, 5433),
3966                    ..Default::default()
3967                })
3968                .into(),
3969            )
3970            .expect("Could not execute request");
3971        assert_eq!(state.count_frontends(), 4);
3972
3973        // remove the HTTP frontend
3974        state
3975            .dispatch(
3976                &RequestType::RemoveHttpFrontend(RequestHttpFrontend {
3977                    cluster_id: Some(String::from("cluster_1")),
3978                    hostname: String::from("example.com"),
3979                    path: PathRule::prefix(String::from("/")),
3980                    address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
3981                    position: RulePosition::Tree.into(),
3982                    ..Default::default()
3983                })
3984                .into(),
3985            )
3986            .expect("Could not execute request");
3987        assert_eq!(state.count_frontends(), 3);
3988    }
3989
3990    // ── helpers ────────────────────────────────────────────────────────────────
3991
3992    fn make_https_listener(address: SocketAddress) -> HttpsListenerConfig {
3993        HttpsListenerConfig {
3994            address,
3995            sticky_name: "SOZUBALANCEID".to_owned(),
3996            front_timeout: 60,
3997            back_timeout: 30,
3998            connect_timeout: 3,
3999            request_timeout: 10,
4000            ..Default::default()
4001        }
4002    }
4003
4004    fn make_http_listener(address: SocketAddress) -> HttpListenerConfig {
4005        HttpListenerConfig {
4006            address,
4007            sticky_name: "SOZUBALANCEID".to_owned(),
4008            front_timeout: 60,
4009            back_timeout: 30,
4010            connect_timeout: 3,
4011            request_timeout: 10,
4012            ..Default::default()
4013        }
4014    }
4015
4016    fn make_tcp_listener(address: SocketAddress) -> TcpListenerConfig {
4017        TcpListenerConfig {
4018            address,
4019            front_timeout: 60,
4020            back_timeout: 30,
4021            connect_timeout: 3,
4022            ..Default::default()
4023        }
4024    }
4025
4026    fn make_udp_listener(address: SocketAddress, active: bool) -> UdpListenerConfig {
4027        UdpListenerConfig {
4028            address,
4029            public_address: None,
4030            front_timeout: 30,
4031            back_timeout: 30,
4032            max_rx_datagram_size: 1500,
4033            max_flows: 0,
4034            active,
4035        }
4036    }
4037
4038    /// Mandatory roundtrip guard for the UDP control-plane data model.
4039    ///
4040    /// 1. Build a state holding an active UDP listener + UDP frontend, run
4041    ///    `generate_requests()`, replay every emitted request into a fresh
4042    ///    `ConfigState`, and assert the two states are byte-for-byte equal.
4043    /// 2. Prove a `diff()` between an empty state and the UDP-bearing state
4044    ///    produces requests that, replayed, reconstruct the UDP listener and
4045    ///    frontend (a hot-add path), and that the reverse diff tears them
4046    ///    down again.
4047    #[test]
4048    fn test_udp_state_roundtrip() {
4049        let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
4050
4051        let mut state = ConfigState::default();
4052        state
4053            .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
4054            .expect("could not add udp listener");
4055        state
4056            .dispatch(
4057                &RequestType::ActivateListener(ActivateListener {
4058                    address,
4059                    proxy: ListenerType::Udp.into(),
4060                    from_scm: false,
4061                })
4062                .into(),
4063            )
4064            .expect("could not activate udp listener");
4065        state
4066            .dispatch(
4067                &RequestType::AddUdpFrontend(RequestUdpFrontend {
4068                    cluster_id: "udp_cluster".to_string(),
4069                    address,
4070                    tags: BTreeMap::from([("owner".to_string(), "team".to_string())]),
4071                })
4072                .into(),
4073            )
4074            .expect("could not add udp frontend");
4075
4076        assert_eq!(state.udp_listeners.len(), 1);
4077        assert!(state.udp_listeners[&address.into()].active);
4078        assert_eq!(
4079            state.udp_fronts.get("udp_cluster").map(Vec::len),
4080            Some(1usize)
4081        );
4082
4083        // `request_counts` is a runtime census side-effect of `dispatch`; it
4084        // diverges by construction whenever the number/shape of replayed
4085        // requests differs from the originals, so compare the logical config
4086        // with the census cleared on both sides.
4087        let logical = |s: &ConfigState| {
4088            let mut c = s.clone();
4089            c.request_counts.clear();
4090            c
4091        };
4092
4093        // 1. generate_requests → replay → equal
4094        let mut replayed = ConfigState::default();
4095        for request in state.generate_requests() {
4096            replayed
4097                .dispatch(&request)
4098                .expect("could not replay generated request");
4099        }
4100        assert_eq!(
4101            logical(&state),
4102            logical(&replayed),
4103            "UDP listener + frontend must survive generate_requests → replay"
4104        );
4105
4106        // 2. diff from empty reconstructs the UDP objects
4107        let empty = ConfigState::default();
4108        let mut from_diff = ConfigState::default();
4109        for request in empty.diff(&state) {
4110            from_diff
4111                .dispatch(&request)
4112                .expect("could not replay diff request");
4113        }
4114        assert_eq!(
4115            logical(&state),
4116            logical(&from_diff),
4117            "diff(empty -> state) must reconstruct the UDP listener + frontend"
4118        );
4119
4120        // reverse diff tears them back down to empty
4121        let mut torn_down = state.clone();
4122        for request in state.diff(&empty) {
4123            torn_down
4124                .dispatch(&request)
4125                .expect("could not replay teardown diff request");
4126        }
4127        assert!(
4128            torn_down.udp_listeners.is_empty(),
4129            "diff(state -> empty) must remove the UDP listener"
4130        );
4131        assert!(
4132            torn_down
4133                .udp_fronts
4134                .get("udp_cluster")
4135                .map(Vec::is_empty)
4136                .unwrap_or(true),
4137            "diff(state -> empty) must remove the UDP frontend"
4138        );
4139
4140        // update path: a partial patch mutates the stored listener in place
4141        state
4142            .dispatch(
4143                &RequestType::UpdateUdpListener(UpdateUdpListenerConfig {
4144                    address,
4145                    max_flows: Some(4096),
4146                    front_timeout: Some(15),
4147                    ..Default::default()
4148                })
4149                .into(),
4150            )
4151            .expect("could not update udp listener");
4152        let updated = &state.udp_listeners[&address.into()];
4153        assert_eq!(updated.max_flows, 4096);
4154        assert_eq!(updated.front_timeout, 15);
4155        assert_eq!(
4156            updated.back_timeout, 30,
4157            "unpatched field must be preserved"
4158        );
4159    }
4160
4161    /// Mandatory roundtrip + identity guard for SNI/ALPN-scoped TCP
4162    /// frontends (sozu-proxy/sozu#1279).
4163    ///
4164    /// 1. Build a state holding a TCP listener with non-default SNI-preread
4165    ///    knobs plus two SNI-scoped frontends on the same address, run
4166    ///    `generate_requests()`, replay every emitted request into a fresh
4167    ///    `ConfigState`, and assert the two states are byte-for-byte equal.
4168    /// 2. Prove `diff()` between an empty state and this state reconstructs
4169    ///    the listener + both frontends (hot-add), and the reverse diff
4170    ///    tears them back down.
4171    /// 3. Prove add/remove identity is `(address, sni, alpn)`: removing one
4172    ///    SNI-scoped sibling leaves the other untouched, and a
4173    ///    address-only removal (mismatched sni/alpn) is rejected rather
4174    ///    than silently nuking every frontend at that address.
4175    #[test]
4176    fn test_tcp_sni_alpn_state_roundtrip() {
4177        let address = SocketAddress::new_v4(127, 0, 0, 1, 6443);
4178
4179        let mut state = ConfigState::default();
4180        state
4181            .dispatch(
4182                &RequestType::AddTcpListener(TcpListenerConfig {
4183                    address,
4184                    front_timeout: 60,
4185                    back_timeout: 30,
4186                    connect_timeout: 3,
4187                    active: true,
4188                    sni_preread_timeout: Some(2),
4189                    sni_preread_max_bytes: Some(8192),
4190                    ..Default::default()
4191                })
4192                .into(),
4193            )
4194            .expect("could not add tcp listener");
4195        state
4196            .dispatch(
4197                &RequestType::ActivateListener(ActivateListener {
4198                    address,
4199                    proxy: ListenerType::Tcp.into(),
4200                    from_scm: false,
4201                })
4202                .into(),
4203            )
4204            .expect("could not activate tcp listener");
4205
4206        let frontend_a = RequestTcpFrontend {
4207            cluster_id: "cluster_sni_a".to_string(),
4208            address,
4209            tags: BTreeMap::new(),
4210            sni: Some("example.com".to_string()),
4211            alpn: vec!["h2".to_string()],
4212        };
4213        let frontend_b = RequestTcpFrontend {
4214            cluster_id: "cluster_sni_b".to_string(),
4215            address,
4216            tags: BTreeMap::new(),
4217            sni: Some("other.example.com".to_string()),
4218            alpn: vec![],
4219        };
4220        state
4221            .dispatch(&RequestType::AddTcpFrontend(frontend_a.clone()).into())
4222            .expect("could not add sni-a tcp frontend");
4223        state
4224            .dispatch(&RequestType::AddTcpFrontend(frontend_b.clone()).into())
4225            .expect("could not add sni-b tcp frontend");
4226
4227        assert_eq!(state.tcp_listeners.len(), 1);
4228        assert!(state.tcp_listeners[&address.into()].active);
4229        assert_eq!(
4230            state.tcp_listeners[&address.into()].sni_preread_timeout,
4231            Some(2)
4232        );
4233        assert_eq!(
4234            state.tcp_listeners[&address.into()].sni_preread_max_bytes,
4235            Some(8192)
4236        );
4237        assert_eq!(state.count_tcp_frontends_raw(), 2);
4238
4239        // `request_counts` is a runtime census side-effect of `dispatch`; it
4240        // diverges by construction whenever the number/shape of replayed
4241        // requests differs from the originals, so compare the logical config
4242        // with the census cleared on both sides.
4243        let logical = |s: &ConfigState| {
4244            let mut c = s.clone();
4245            c.request_counts.clear();
4246            c
4247        };
4248
4249        // 1. generate_requests → replay → equal
4250        let mut replayed = ConfigState::default();
4251        for request in state.generate_requests() {
4252            replayed
4253                .dispatch(&request)
4254                .expect("could not replay generated request");
4255        }
4256        assert_eq!(
4257            logical(&state),
4258            logical(&replayed),
4259            "SNI/ALPN TCP listener + frontends must survive generate_requests → replay"
4260        );
4261
4262        // 2. diff from empty reconstructs the TCP objects
4263        let empty = ConfigState::default();
4264        let mut from_diff = ConfigState::default();
4265        for request in empty.diff(&state) {
4266            from_diff
4267                .dispatch(&request)
4268                .expect("could not replay diff request");
4269        }
4270        assert_eq!(
4271            logical(&state),
4272            logical(&from_diff),
4273            "diff(empty -> state) must reconstruct the tcp listener + both sni frontends"
4274        );
4275
4276        // reverse diff tears them back down to empty
4277        let mut torn_down = state.clone();
4278        for request in state.diff(&empty) {
4279            torn_down
4280                .dispatch(&request)
4281                .expect("could not replay teardown diff request");
4282        }
4283        assert!(
4284            torn_down.tcp_listeners.is_empty(),
4285            "diff(state -> empty) must remove the tcp listener"
4286        );
4287        assert_eq!(
4288            torn_down.count_tcp_frontends_raw(),
4289            0,
4290            "diff(state -> empty) must remove both sni frontends"
4291        );
4292
4293        // 3a. an address-only removal (mismatched sni/alpn) must not match
4294        // either sibling — identity is (address, sni, alpn), not address
4295        // alone.
4296        let mismatched_removal = state.dispatch(
4297            &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
4298                cluster_id: "cluster_sni_a".to_string(),
4299                address,
4300                tags: BTreeMap::new(),
4301                sni: None,
4302                alpn: vec![],
4303            })
4304            .into(),
4305        );
4306        assert!(
4307            mismatched_removal.is_err(),
4308            "removing by address alone (no matching sni/alpn) must be rejected"
4309        );
4310        assert_eq!(
4311            state.count_tcp_frontends_raw(),
4312            2,
4313            "a rejected mismatched removal must not change either sibling"
4314        );
4315
4316        // 3b. removing the exact (address, sni, alpn) identity of frontend_a
4317        // must drop only that entry and leave frontend_b untouched.
4318        state
4319            .dispatch(&RequestType::RemoveTcpFrontend(frontend_a).into())
4320            .expect("could not remove sni-a tcp frontend");
4321        assert_eq!(state.count_tcp_frontends_raw(), 1);
4322        let remaining = state
4323            .tcp_fronts
4324            .get("cluster_sni_b")
4325            .expect("cluster_sni_b bucket must survive");
4326        assert_eq!(remaining.len(), 1);
4327        assert_eq!(remaining[0].sni, Some("other.example.com".to_string()));
4328    }
4329
4330    // ── master-side TCP frontend admission parity (sozu-proxy/sozu#1290) ──────
4331    //
4332    // The command server mutates master `ConfigState` before fanning a
4333    // request out to workers, and a worker NACK never rolls the master back.
4334    // These tests prove the master rejects everything the worker's
4335    // `validate_new_tcp_front` (`lib/src/tcp.rs`) would reject, BEFORE any
4336    // mutation -- every rejection case asserts `hash_state()` and
4337    // `count_tcp_frontends_raw()` (plus the relevant bucket lengths) are
4338    // byte-for-byte unchanged, not just that the call returned `Err`.
4339
4340    fn add_tcp_test_cluster(state: &mut ConfigState, cluster_id: &str) {
4341        state
4342            .dispatch(
4343                &RequestType::AddCluster(Cluster {
4344                    cluster_id: cluster_id.to_string(),
4345                    sticky_session: false,
4346                    https_redirect: false,
4347                    ..Default::default()
4348                })
4349                .into(),
4350            )
4351            .expect("could not add cluster");
4352    }
4353
4354    /// The (address, sni, alpn) identity check must span ALL clusters at a
4355    /// listener address, not just the candidate's own `cluster_id` bucket --
4356    /// otherwise the same identity under a different cluster_id is silently
4357    /// accepted by the master and then rejected by every worker.
4358    #[test]
4359    fn add_tcp_frontend_rejects_cross_cluster_duplicate_identity() {
4360        let address = SocketAddress::new_v4(127, 0, 0, 1, 9001);
4361        let mut state = ConfigState::new();
4362        add_tcp_test_cluster(&mut state, "cluster_x");
4363        add_tcp_test_cluster(&mut state, "cluster_y");
4364
4365        state
4366            .dispatch(
4367                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4368                    cluster_id: "cluster_x".to_string(),
4369                    address,
4370                    tags: BTreeMap::new(),
4371                    sni: Some("example.com".to_string()),
4372                    alpn: vec!["h2".to_string()],
4373                })
4374                .into(),
4375            )
4376            .expect("could not add cluster_x tcp frontend");
4377
4378        let hash_before = state.hash_state();
4379        let count_before = state.count_tcp_frontends_raw();
4380
4381        let result = state.dispatch(
4382            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4383                cluster_id: "cluster_y".to_string(),
4384                address,
4385                tags: BTreeMap::new(),
4386                sni: Some("example.com".to_string()),
4387                alpn: vec!["h2".to_string()],
4388            })
4389            .into(),
4390        );
4391        assert!(
4392            result.is_err(),
4393            "the same (address, sni, alpn) under a different cluster_id must be rejected, \
4394             got: {result:?}"
4395        );
4396        assert_eq!(
4397            state.hash_state(),
4398            hash_before,
4399            "a rejected cross-cluster duplicate must not change the state hash"
4400        );
4401        assert_eq!(
4402            state.count_tcp_frontends_raw(),
4403            count_before,
4404            "a rejected cross-cluster duplicate must not change the frontend count"
4405        );
4406        assert_eq!(state.tcp_fronts.get("cluster_x").map(Vec::len), Some(1));
4407        assert_eq!(
4408            state.tcp_fronts.get("cluster_y").map(Vec::len).unwrap_or(0),
4409            0
4410        );
4411    }
4412
4413    /// ALPN identity is a *set*: the same protocols in a different order
4414    /// must still collide as a duplicate, even within one cluster's bucket.
4415    #[test]
4416    fn add_tcp_frontend_rejects_reordered_alpn_duplicate() {
4417        let address = SocketAddress::new_v4(127, 0, 0, 1, 9002);
4418        let mut state = ConfigState::new();
4419        add_tcp_test_cluster(&mut state, "cluster_reorder");
4420
4421        state
4422            .dispatch(
4423                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4424                    cluster_id: "cluster_reorder".to_string(),
4425                    address,
4426                    tags: BTreeMap::new(),
4427                    sni: Some("example.com".to_string()),
4428                    alpn: vec!["h2".to_string(), "http/1.1".to_string()],
4429                })
4430                .into(),
4431            )
4432            .expect("could not add first tcp frontend");
4433
4434        let hash_before = state.hash_state();
4435        let count_before = state.count_tcp_frontends_raw();
4436
4437        let result = state.dispatch(
4438            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4439                cluster_id: "cluster_reorder".to_string(),
4440                address,
4441                tags: BTreeMap::new(),
4442                sni: Some("example.com".to_string()),
4443                alpn: vec!["http/1.1".to_string(), "h2".to_string()],
4444            })
4445            .into(),
4446        );
4447        assert!(
4448            result.is_err(),
4449            "the same ALPN set in a different order must be rejected as a duplicate \
4450             identity, got: {result:?}"
4451        );
4452        assert_eq!(state.hash_state(), hash_before);
4453        assert_eq!(state.count_tcp_frontends_raw(), count_before);
4454        assert_eq!(
4455            state.tcp_fronts.get("cluster_reorder").map(Vec::len),
4456            Some(1)
4457        );
4458    }
4459
4460    /// Symmetric counterpart: removal must also treat ALPN as a set, so a
4461    /// request differing only in protocol order still matches the stored
4462    /// (canonical) frontend instead of falling through to `NoChange`.
4463    #[test]
4464    fn remove_tcp_frontend_matches_reordered_alpn() {
4465        let address = SocketAddress::new_v4(127, 0, 0, 1, 9003);
4466        let mut state = ConfigState::new();
4467        add_tcp_test_cluster(&mut state, "cluster_remove_reorder");
4468
4469        state
4470            .dispatch(
4471                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4472                    cluster_id: "cluster_remove_reorder".to_string(),
4473                    address,
4474                    tags: BTreeMap::new(),
4475                    sni: Some("example.com".to_string()),
4476                    alpn: vec!["h2".to_string(), "http/1.1".to_string()],
4477                })
4478                .into(),
4479            )
4480            .expect("could not add tcp frontend");
4481
4482        let result = state.dispatch(
4483            &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
4484                cluster_id: "cluster_remove_reorder".to_string(),
4485                address,
4486                tags: BTreeMap::new(),
4487                sni: Some("example.com".to_string()),
4488                alpn: vec!["http/1.1".to_string(), "h2".to_string()],
4489            })
4490            .into(),
4491        );
4492        assert!(
4493            result.is_ok(),
4494            "removing with the same ALPN set in a different order must succeed, got: {result:?}"
4495        );
4496        assert_eq!(state.count_tcp_frontends_raw(), 0);
4497    }
4498
4499    /// ALPN-set overlap on the same (address, normalized sni) must be
4500    /// rejected even across different clusters -- the previous check never
4501    /// looked outside the candidate's own cluster bucket at all.
4502    #[test]
4503    fn add_tcp_frontend_rejects_alpn_overlap_across_clusters_same_sni() {
4504        let address = SocketAddress::new_v4(127, 0, 0, 1, 9004);
4505        let mut state = ConfigState::new();
4506        add_tcp_test_cluster(&mut state, "cluster_overlap_a");
4507        add_tcp_test_cluster(&mut state, "cluster_overlap_b");
4508
4509        state
4510            .dispatch(
4511                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4512                    cluster_id: "cluster_overlap_a".to_string(),
4513                    address,
4514                    tags: BTreeMap::new(),
4515                    sni: Some("example.com".to_string()),
4516                    alpn: vec!["h2".to_string()],
4517                })
4518                .into(),
4519            )
4520            .expect("could not add cluster_overlap_a tcp frontend");
4521
4522        let hash_before = state.hash_state();
4523        let count_before = state.count_tcp_frontends_raw();
4524
4525        let result = state.dispatch(
4526            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4527                cluster_id: "cluster_overlap_b".to_string(),
4528                address,
4529                tags: BTreeMap::new(),
4530                sni: Some("example.com".to_string()),
4531                alpn: vec!["h2".to_string(), "http/1.1".to_string()],
4532            })
4533            .into(),
4534        );
4535        assert!(
4536            result.is_err(),
4537            "an overlapping ALPN set on the same (address, sni) under a different cluster_id \
4538             must be rejected, got: {result:?}"
4539        );
4540        assert_eq!(state.hash_state(), hash_before);
4541        assert_eq!(state.count_tcp_frontends_raw(), count_before);
4542        assert_eq!(
4543            state
4544                .tcp_fronts
4545                .get("cluster_overlap_b")
4546                .map(Vec::len)
4547                .unwrap_or(0),
4548            0
4549        );
4550    }
4551
4552    /// Listener-wide no-SNI/SNI mixing must be enforced in both directions,
4553    /// across cluster buckets.
4554    #[test]
4555    fn add_tcp_frontend_rejects_no_sni_sni_mixing_both_directions() {
4556        let address_a = SocketAddress::new_v4(127, 0, 0, 1, 9005);
4557        let address_b = SocketAddress::new_v4(127, 0, 0, 1, 9006);
4558        let mut state = ConfigState::new();
4559        add_tcp_test_cluster(&mut state, "cluster_mix_1");
4560        add_tcp_test_cluster(&mut state, "cluster_mix_2");
4561        add_tcp_test_cluster(&mut state, "cluster_mix_3");
4562        add_tcp_test_cluster(&mut state, "cluster_mix_4");
4563
4564        // Direction 1: a no-SNI frontend exists; adding an SNI-scoped
4565        // frontend at the SAME address must be rejected.
4566        state
4567            .dispatch(
4568                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4569                    cluster_id: "cluster_mix_1".to_string(),
4570                    address: address_a,
4571                    tags: BTreeMap::new(),
4572                    sni: None,
4573                    alpn: vec![],
4574                })
4575                .into(),
4576            )
4577            .expect("could not add no-sni tcp frontend");
4578        let hash_before_a = state.hash_state();
4579        let count_before_a = state.count_tcp_frontends_raw();
4580        let result_a = state.dispatch(
4581            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4582                cluster_id: "cluster_mix_2".to_string(),
4583                address: address_a,
4584                tags: BTreeMap::new(),
4585                sni: Some("example.com".to_string()),
4586                alpn: vec![],
4587            })
4588            .into(),
4589        );
4590        assert!(
4591            result_a.is_err(),
4592            "an SNI-scoped frontend must not be added to a listener with an existing no-SNI \
4593             front, got: {result_a:?}"
4594        );
4595        assert_eq!(state.hash_state(), hash_before_a);
4596        assert_eq!(state.count_tcp_frontends_raw(), count_before_a);
4597
4598        // Direction 2: an SNI-scoped frontend exists; adding a no-SNI
4599        // frontend at the SAME address must be rejected.
4600        state
4601            .dispatch(
4602                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4603                    cluster_id: "cluster_mix_3".to_string(),
4604                    address: address_b,
4605                    tags: BTreeMap::new(),
4606                    sni: Some("example.com".to_string()),
4607                    alpn: vec![],
4608                })
4609                .into(),
4610            )
4611            .expect("could not add sni-scoped tcp frontend");
4612        let hash_before_b = state.hash_state();
4613        let count_before_b = state.count_tcp_frontends_raw();
4614        let result_b = state.dispatch(
4615            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4616                cluster_id: "cluster_mix_4".to_string(),
4617                address: address_b,
4618                tags: BTreeMap::new(),
4619                sni: None,
4620                alpn: vec![],
4621            })
4622            .into(),
4623        );
4624        assert!(
4625            result_b.is_err(),
4626            "a no-SNI frontend must not be added to a listener with existing SNI-scoped \
4627             fronts, got: {result_b:?}"
4628        );
4629        assert_eq!(state.hash_state(), hash_before_b);
4630        assert_eq!(state.count_tcp_frontends_raw(), count_before_b);
4631    }
4632
4633    /// A malformed SNI pattern must be rejected by the master itself, not
4634    /// merely by config-load's `to_tcp_front` -- a raw `AddTcpFrontend` over
4635    /// the command socket, or a `LoadState` replay, bypasses `config.rs`
4636    /// entirely.
4637    #[test]
4638    fn add_tcp_frontend_rejects_malformed_sni_shape() {
4639        let address = SocketAddress::new_v4(127, 0, 0, 1, 9007);
4640        let mut state = ConfigState::new();
4641        add_tcp_test_cluster(&mut state, "cluster_malformed");
4642
4643        let hash_before = state.hash_state();
4644        let count_before = state.count_tcp_frontends_raw();
4645
4646        let result = state.dispatch(
4647            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4648                cluster_id: "cluster_malformed".to_string(),
4649                address,
4650                tags: BTreeMap::new(),
4651                // Leading empty label -- rejected by `validate_sni_pattern`.
4652                sni: Some(".example.com".to_string()),
4653                alpn: vec![],
4654            })
4655            .into(),
4656        );
4657        assert!(
4658            result.is_err(),
4659            "a malformed SNI pattern must be rejected by the master, not merely by \
4660             config-load, got: {result:?}"
4661        );
4662        assert_eq!(state.hash_state(), hash_before);
4663        assert_eq!(state.count_tcp_frontends_raw(), count_before);
4664        assert_eq!(
4665            state
4666                .tcp_fronts
4667                .get("cluster_malformed")
4668                .map(Vec::len)
4669                .unwrap_or(0),
4670            0
4671        );
4672    }
4673
4674    /// A non-empty `alpn` without `sni` must be rejected by the master:
4675    /// alpn only matches within an SNI-scoped preread, so a no-SNI frontend
4676    /// would silently ignore its alpn list. Config-load (`to_tcp_front`) and
4677    /// the worker both reject this shape; a raw `AddTcpFrontend` over the
4678    /// command socket must not slip past the master.
4679    #[test]
4680    fn add_tcp_frontend_rejects_alpn_without_sni() {
4681        let address = SocketAddress::new_v4(127, 0, 0, 1, 9009);
4682        let mut state = ConfigState::new();
4683        add_tcp_test_cluster(&mut state, "cluster_alpn_no_sni");
4684
4685        let hash_before = state.hash_state();
4686        let count_before = state.count_tcp_frontends_raw();
4687
4688        let result = state.dispatch(
4689            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4690                cluster_id: "cluster_alpn_no_sni".to_string(),
4691                address,
4692                tags: BTreeMap::new(),
4693                sni: None,
4694                alpn: vec!["h2".to_string()],
4695            })
4696            .into(),
4697        );
4698        assert!(
4699            result.is_err(),
4700            "a non-empty alpn without sni must be rejected by the master, got: {result:?}"
4701        );
4702        assert_eq!(
4703            state.hash_state(),
4704            hash_before,
4705            "a rejected alpn-without-sni add must not change the state hash"
4706        );
4707        assert_eq!(
4708            state.count_tcp_frontends_raw(),
4709            count_before,
4710            "a rejected alpn-without-sni add must not change the frontend count"
4711        );
4712        assert_eq!(
4713            state
4714                .tcp_fronts
4715                .get("cluster_alpn_no_sni")
4716                .map(Vec::len)
4717                .unwrap_or(0),
4718            0
4719        );
4720    }
4721
4722    /// Worker-parity guard: an exact catch-all and a wildcard catch-all on
4723    /// the SAME address are DISTINCT (address, sni) keys and must both be
4724    /// accepted -- the listener-wide admission checks above must not
4725    /// over-forbid this legal combination by conflating exact and wildcard
4726    /// SNI as the same key.
4727    #[test]
4728    fn add_tcp_frontend_accepts_exact_and_wildcard_catch_all_siblings() {
4729        let address = SocketAddress::new_v4(127, 0, 0, 1, 9008);
4730        let mut state = ConfigState::new();
4731        add_tcp_test_cluster(&mut state, "cluster_exact_catchall");
4732        add_tcp_test_cluster(&mut state, "cluster_wildcard_catchall");
4733
4734        state
4735            .dispatch(
4736                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4737                    cluster_id: "cluster_exact_catchall".to_string(),
4738                    address,
4739                    tags: BTreeMap::new(),
4740                    sni: Some("example.com".to_string()),
4741                    alpn: vec![],
4742                })
4743                .into(),
4744            )
4745            .expect("could not add exact catch-all tcp frontend");
4746
4747        let result = state.dispatch(
4748            &RequestType::AddTcpFrontend(RequestTcpFrontend {
4749                cluster_id: "cluster_wildcard_catchall".to_string(),
4750                address,
4751                tags: BTreeMap::new(),
4752                sni: Some("*.example.com".to_string()),
4753                alpn: vec![],
4754            })
4755            .into(),
4756        );
4757        assert!(
4758            result.is_ok(),
4759            "an exact catch-all and a wildcard catch-all on the same address must both be \
4760             accepted, got: {result:?}"
4761        );
4762        assert_eq!(state.count_tcp_frontends_raw(), 2);
4763    }
4764
4765    /// `list_frontends` must surface UDP frontends alongside TCP ones. The
4766    /// proto `FrontendFilters` has no `udp` flag, so UDP rides the default
4767    /// (all-pass) and `tcp` filters; a `domain` filter excludes both.
4768    #[test]
4769    fn list_frontends_includes_udp() {
4770        let tcp_addr = SocketAddress::new_v4(0, 0, 0, 0, 6379);
4771        let udp_addr = SocketAddress::new_v4(0, 0, 0, 0, 5353);
4772
4773        let mut state = ConfigState::default();
4774        state
4775            .dispatch(
4776                &RequestType::AddTcpFrontend(RequestTcpFrontend {
4777                    cluster_id: "tcp_cluster".to_string(),
4778                    address: tcp_addr,
4779                    ..Default::default()
4780                })
4781                .into(),
4782            )
4783            .expect("could not add tcp frontend");
4784        state
4785            .dispatch(
4786                &RequestType::AddUdpFrontend(RequestUdpFrontend {
4787                    cluster_id: "udp_cluster".to_string(),
4788                    address: udp_addr,
4789                    ..Default::default()
4790                })
4791                .into(),
4792            )
4793            .expect("could not add udp frontend");
4794
4795        // default filters (all false, no domain) → list everything, incl. UDP
4796        let all = state.list_frontends(FrontendFilters::default());
4797        assert_eq!(all.tcp_frontends.len(), 1, "tcp frontend must be listed");
4798        assert_eq!(
4799            all.udp_frontends.len(),
4800            1,
4801            "udp frontend must be listed under the default all-pass path"
4802        );
4803        assert_eq!(all.udp_frontends[0].cluster_id, "udp_cluster");
4804        assert_eq!(all.udp_frontends[0].address, udp_addr);
4805
4806        // explicit `tcp` filter surfaces both TCP and UDP (no `udp` flag exists)
4807        let tcp_only = state.list_frontends(FrontendFilters {
4808            tcp: true,
4809            ..Default::default()
4810        });
4811        assert_eq!(tcp_only.tcp_frontends.len(), 1);
4812        assert_eq!(
4813            tcp_only.udp_frontends.len(),
4814            1,
4815            "udp frontends ride the tcp filter"
4816        );
4817
4818        // an `http` filter excludes both TCP and UDP
4819        let http_only = state.list_frontends(FrontendFilters {
4820            http: true,
4821            ..Default::default()
4822        });
4823        assert!(http_only.tcp_frontends.is_empty());
4824        assert!(http_only.udp_frontends.is_empty());
4825
4826        // a `domain` filter excludes UDP (no hostname), matching TCP behaviour
4827        let domain_filtered = state.list_frontends(FrontendFilters {
4828            domain: Some("example.com".to_string()),
4829            ..Default::default()
4830        });
4831        assert!(domain_filtered.tcp_frontends.is_empty());
4832        assert!(domain_filtered.udp_frontends.is_empty());
4833    }
4834
4835    // ── update_https_listener ──────────────────────────────────────────────────
4836
4837    /// Happy path: patching two H2 flood knobs updates the map entry; all other
4838    /// fields are left untouched.
4839    #[test]
4840    fn update_https_listener_happy_path_h2_knobs() {
4841        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
4842        let mut state = ConfigState::new();
4843        state
4844            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
4845            .unwrap();
4846
4847        let patch = UpdateHttpsListenerConfig {
4848            address: addr,
4849            h2_max_rst_stream_per_window: Some(50),
4850            h2_max_ping_per_window: Some(20),
4851            ..Default::default()
4852        };
4853        state
4854            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
4855            .expect("update must succeed");
4856
4857        let listener = state
4858            .https_listeners
4859            .get(&SocketAddr::from(addr))
4860            .expect("listener must be present");
4861        assert_eq!(listener.h2_max_rst_stream_per_window, Some(50));
4862        assert_eq!(listener.h2_max_ping_per_window, Some(20));
4863        // Untouched fields must be unchanged
4864        assert_eq!(listener.front_timeout, 60);
4865        assert_eq!(listener.h2_max_settings_per_window, None);
4866    }
4867
4868    /// NotFound: patching a listener address that was never registered returns
4869    /// `StateError::NotFound`.
4870    #[test]
4871    fn update_https_listener_not_found() {
4872        let mut state = ConfigState::new();
4873        let patch = UpdateHttpsListenerConfig {
4874            address: SocketAddress::new_v4(1, 2, 3, 4, 9999),
4875            h2_max_rst_stream_per_window: Some(50),
4876            ..Default::default()
4877        };
4878        let err = state
4879            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
4880            .unwrap_err();
4881        assert!(
4882            matches!(
4883                err,
4884                StateError::NotFound {
4885                    kind: ObjectKind::HttpsListener,
4886                    ..
4887                }
4888            ),
4889            "expected NotFound, got: {err}"
4890        );
4891    }
4892
4893    /// No-op: a patch with only `address` set (all options None) is Ok and does
4894    /// not change any field.
4895    #[test]
4896    fn update_https_listener_noop_patch() {
4897        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
4898        let mut state = ConfigState::new();
4899        let original = make_https_listener(addr);
4900        state
4901            .dispatch(&RequestType::AddHttpsListener(original.clone()).into())
4902            .unwrap();
4903
4904        let patch = UpdateHttpsListenerConfig {
4905            address: addr,
4906            ..Default::default()
4907        };
4908        state
4909            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
4910            .expect("no-op patch must succeed");
4911
4912        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
4913        assert_eq!(listener.front_timeout, original.front_timeout);
4914        assert_eq!(
4915            listener.h2_max_rst_stream_per_window,
4916            original.h2_max_rst_stream_per_window
4917        );
4918    }
4919
4920    /// InvalidValue: setting a flood knob to 0 must be rejected.
4921    #[test]
4922    fn update_https_listener_invalid_value_flood_knob_zero() {
4923        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
4924        let mut state = ConfigState::new();
4925        state
4926            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
4927            .unwrap();
4928
4929        let patch = UpdateHttpsListenerConfig {
4930            address: addr,
4931            h2_max_rst_stream_per_window: Some(0),
4932            ..Default::default()
4933        };
4934        let err = state
4935            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
4936            .unwrap_err();
4937        assert!(
4938            matches!(
4939                err,
4940                StateError::InvalidValue {
4941                    field: "h2_max_rst_stream_per_window",
4942                    ..
4943                }
4944            ),
4945            "expected InvalidValue for flood knob 0, got: {err}"
4946        );
4947    }
4948
4949    /// AddCluster: an inline `cluster.health_check` with a CRLF-bearing URI
4950    /// must be rejected before the cluster lands in `ConfigState`. Without
4951    /// this guard, TOML reload / SaveState / direct API AddCluster requests
4952    /// bypass the SetHealthCheck-side check and let an attacker-controlled
4953    /// health-check URI smuggle CR/LF into outbound HTTP/1.1 probes.
4954    #[test]
4955    fn add_cluster_invalid_health_check_uri_rejected() {
4956        use crate::proto::command::HealthCheckConfig;
4957
4958        let mut state = ConfigState::new();
4959        let err = state
4960            .dispatch(
4961                &RequestType::AddCluster(Cluster {
4962                    cluster_id: String::from("evil_cluster"),
4963                    health_check: Some(HealthCheckConfig {
4964                        uri: String::from("/foo\r\nGET /admin"),
4965                        interval: 5_000,
4966                        timeout: 1_000,
4967                        healthy_threshold: 2,
4968                        unhealthy_threshold: 2,
4969                        ..Default::default()
4970                    }),
4971                    ..Default::default()
4972                })
4973                .into(),
4974            )
4975            .unwrap_err();
4976
4977        assert!(
4978            matches!(
4979                err,
4980                StateError::InvalidValue {
4981                    field: "health_check",
4982                    ..
4983                }
4984            ),
4985            "expected InvalidValue for CRLF-bearing health-check URI, got: {err}"
4986        );
4987        assert!(
4988            !state.clusters.contains_key("evil_cluster"),
4989            "cluster must not be inserted when health_check fails validation",
4990        );
4991    }
4992
4993    /// ALPN validation: reject unknown ALPN values.
4994    #[test]
4995    fn update_https_listener_alpn_unknown_value_rejected() {
4996        use crate::proto::command::AlpnProtocols;
4997
4998        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
4999        let mut state = ConfigState::new();
5000        state
5001            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5002            .unwrap();
5003
5004        let patch = UpdateHttpsListenerConfig {
5005            address: addr,
5006            alpn_protocols: Some(AlpnProtocols {
5007                values: vec!["h3".to_owned()],
5008            }),
5009            ..Default::default()
5010        };
5011        let err = state
5012            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5013            .unwrap_err();
5014        assert!(
5015            matches!(
5016                err,
5017                StateError::InvalidValue {
5018                    field: "alpn_protocols",
5019                    ..
5020                }
5021            ),
5022            "expected InvalidValue for unknown ALPN, got: {err}"
5023        );
5024    }
5025
5026    /// ALPN validation: empty values vec = reset to default, must be accepted.
5027    #[test]
5028    fn update_https_listener_alpn_empty_reset_accepted() {
5029        use crate::proto::command::AlpnProtocols;
5030
5031        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5032        let mut state = ConfigState::new();
5033        let mut listener = make_https_listener(addr);
5034        listener.alpn_protocols = vec!["h2".to_owned()];
5035        state
5036            .dispatch(&RequestType::AddHttpsListener(listener).into())
5037            .unwrap();
5038
5039        let patch = UpdateHttpsListenerConfig {
5040            address: addr,
5041            alpn_protocols: Some(AlpnProtocols { values: vec![] }),
5042            ..Default::default()
5043        };
5044        state
5045            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5046            .expect("empty ALPN reset must succeed");
5047
5048        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
5049        assert!(
5050            listener.alpn_protocols.is_empty(),
5051            "ALPN must have been reset to empty"
5052        );
5053    }
5054
5055    /// ALPN validation: valid values ["h2", "http/1.1"] must be accepted.
5056    #[test]
5057    fn update_https_listener_alpn_valid_values_accepted() {
5058        use crate::proto::command::AlpnProtocols;
5059
5060        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5061        let mut state = ConfigState::new();
5062        state
5063            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5064            .unwrap();
5065
5066        let patch = UpdateHttpsListenerConfig {
5067            address: addr,
5068            alpn_protocols: Some(AlpnProtocols {
5069                values: vec!["h2".to_owned(), "http/1.1".to_owned()],
5070            }),
5071            ..Default::default()
5072        };
5073        state
5074            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5075            .expect("valid ALPN must be accepted");
5076
5077        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
5078        assert_eq!(listener.alpn_protocols, vec!["h2", "http/1.1"]);
5079    }
5080
5081    /// ALPN absent wrapper: when `alpn_protocols` is None in the patch, the
5082    /// listener's ALPN field must not be touched.
5083    #[test]
5084    fn update_https_listener_alpn_absent_preserves_existing() {
5085        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5086        let mut state = ConfigState::new();
5087        let mut listener = make_https_listener(addr);
5088        listener.alpn_protocols = vec!["h2".to_owned()];
5089        state
5090            .dispatch(&RequestType::AddHttpsListener(listener).into())
5091            .unwrap();
5092
5093        // patch with no alpn_protocols field
5094        let patch = UpdateHttpsListenerConfig {
5095            address: addr,
5096            front_timeout: Some(10),
5097            ..Default::default()
5098        };
5099        state
5100            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5101            .unwrap();
5102
5103        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
5104        assert_eq!(
5105            listener.alpn_protocols,
5106            vec!["h2"],
5107            "ALPN must be preserved when not patched"
5108        );
5109    }
5110
5111    /// sozu_id_header validation: empty string must be rejected.
5112    #[test]
5113    fn update_https_listener_sozu_id_header_empty_rejected() {
5114        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5115        let mut state = ConfigState::new();
5116        state
5117            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5118            .unwrap();
5119
5120        let patch = UpdateHttpsListenerConfig {
5121            address: addr,
5122            sozu_id_header: Some(String::new()),
5123            ..Default::default()
5124        };
5125        let err = state
5126            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5127            .unwrap_err();
5128        assert!(
5129            matches!(
5130                err,
5131                StateError::InvalidValue {
5132                    field: "sozu_id_header",
5133                    ..
5134                }
5135            ),
5136            "expected InvalidValue for empty header name, got: {err}"
5137        );
5138    }
5139
5140    /// sozu_id_header validation: value containing colon must be rejected.
5141    #[test]
5142    fn update_https_listener_sozu_id_header_colon_rejected() {
5143        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5144        let mut state = ConfigState::new();
5145        state
5146            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5147            .unwrap();
5148
5149        let patch = UpdateHttpsListenerConfig {
5150            address: addr,
5151            sozu_id_header: Some("bad: value".to_owned()),
5152            ..Default::default()
5153        };
5154        let err = state
5155            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5156            .unwrap_err();
5157        assert!(
5158            matches!(
5159                err,
5160                StateError::InvalidValue {
5161                    field: "sozu_id_header",
5162                    ..
5163                }
5164            ),
5165            "expected InvalidValue for header name with colon, got: {err}"
5166        );
5167    }
5168
5169    /// sozu_id_header validation: well-formed token must be accepted.
5170    #[test]
5171    fn update_https_listener_sozu_id_header_valid_accepted() {
5172        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5173        let mut state = ConfigState::new();
5174        state
5175            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5176            .unwrap();
5177
5178        let patch = UpdateHttpsListenerConfig {
5179            address: addr,
5180            sozu_id_header: Some("X-Edge-Id".to_owned()),
5181            ..Default::default()
5182        };
5183        state
5184            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5185            .expect("valid header name must be accepted");
5186
5187        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
5188        assert_eq!(listener.sozu_id_header.as_deref(), Some("X-Edge-Id"));
5189    }
5190
5191    /// h2_graceful_shutdown_deadline_seconds = 0 must be allowed (means "wait forever").
5192    #[test]
5193    fn update_https_listener_graceful_shutdown_zero_allowed() {
5194        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
5195        let mut state = ConfigState::new();
5196        state
5197            .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
5198            .unwrap();
5199
5200        let patch = UpdateHttpsListenerConfig {
5201            address: addr,
5202            h2_graceful_shutdown_deadline_seconds: Some(0),
5203            ..Default::default()
5204        };
5205        state
5206            .dispatch(&RequestType::UpdateHttpsListener(patch).into())
5207            .expect("graceful_shutdown_deadline=0 must be allowed");
5208
5209        let listener = state.https_listeners.get(&SocketAddr::from(addr)).unwrap();
5210        assert_eq!(listener.h2_graceful_shutdown_deadline_seconds, Some(0));
5211    }
5212
5213    // ── update_http_listener ───────────────────────────────────────────────────
5214
5215    /// Happy path for HTTP listener patch.
5216    #[test]
5217    fn update_http_listener_happy_path() {
5218        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
5219        let mut state = ConfigState::new();
5220        state
5221            .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
5222            .unwrap();
5223
5224        let patch = UpdateHttpListenerConfig {
5225            address: addr,
5226            front_timeout: Some(15),
5227            h2_max_rst_stream_per_window: Some(25),
5228            ..Default::default()
5229        };
5230        state
5231            .dispatch(&RequestType::UpdateHttpListener(patch).into())
5232            .expect("HTTP update must succeed");
5233
5234        let listener = state.http_listeners.get(&SocketAddr::from(addr)).unwrap();
5235        assert_eq!(listener.front_timeout, 15);
5236        assert_eq!(listener.h2_max_rst_stream_per_window, Some(25));
5237        // untouched
5238        assert_eq!(listener.back_timeout, 30);
5239    }
5240
5241    /// HTTP listener: flood knob 0 is rejected.
5242    #[test]
5243    fn update_http_listener_flood_knob_zero_rejected() {
5244        let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
5245        let mut state = ConfigState::new();
5246        state
5247            .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
5248            .unwrap();
5249
5250        let patch = UpdateHttpListenerConfig {
5251            address: addr,
5252            h2_max_window_update_stream0_per_window: Some(0),
5253            ..Default::default()
5254        };
5255        let err = state
5256            .dispatch(&RequestType::UpdateHttpListener(patch).into())
5257            .unwrap_err();
5258        assert!(
5259            matches!(
5260                err,
5261                StateError::InvalidValue {
5262                    field: "h2_max_window_update_stream0_per_window",
5263                    ..
5264                }
5265            ),
5266            "expected InvalidValue, got: {err}"
5267        );
5268    }
5269
5270    // ── update_tcp_listener ────────────────────────────────────────────────────
5271
5272    /// Happy path for TCP listener patch.
5273    #[test]
5274    fn update_tcp_listener_happy_path() {
5275        let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
5276        let mut state = ConfigState::new();
5277        state
5278            .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
5279            .unwrap();
5280
5281        let patch = UpdateTcpListenerConfig {
5282            address: addr,
5283            front_timeout: Some(5),
5284            ..Default::default()
5285        };
5286        state
5287            .dispatch(&RequestType::UpdateTcpListener(patch).into())
5288            .expect("TCP update must succeed");
5289
5290        let listener = state.tcp_listeners.get(&SocketAddr::from(addr)).unwrap();
5291        assert_eq!(listener.front_timeout, 5);
5292        assert_eq!(listener.back_timeout, 30); // untouched
5293    }
5294
5295    /// TCP listener: NotFound when address is unknown.
5296    #[test]
5297    fn update_tcp_listener_not_found() {
5298        let mut state = ConfigState::new();
5299        let patch = UpdateTcpListenerConfig {
5300            address: SocketAddress::new_v4(9, 9, 9, 9, 9999),
5301            front_timeout: Some(5),
5302            ..Default::default()
5303        };
5304        let err = state
5305            .dispatch(&RequestType::UpdateTcpListener(patch).into())
5306            .unwrap_err();
5307        assert!(
5308            matches!(
5309                err,
5310                StateError::NotFound {
5311                    kind: ObjectKind::TcpListener,
5312                    ..
5313                }
5314            ),
5315            "expected NotFound, got: {err}"
5316        );
5317    }
5318
5319    /// `ConfigState::dispatch` MUST treat `SetMetricDetail` as a
5320    /// runtime-only verb (no persisted state mutation). A future
5321    /// refactor that drops the variant from the no-op match arm and
5322    /// falls through to the catch-all would silently re-break the
5323    /// SetMetricDetail dispatch path with `UndispatchableRequest`.
5324    #[test]
5325    fn dispatch_passes_through_set_metric_detail() {
5326        use crate::proto::command::{MetricDetail, SetMetricDetail};
5327        let mut state = ConfigState::new();
5328        let req: Request = RequestType::SetMetricDetail(SetMetricDetail {
5329            client_id: "test:1".to_owned(),
5330            detail: Some(MetricDetail::DetailBackend as i32),
5331            ttl_seconds: Some(60),
5332            clear: Some(false),
5333            reason: Some("regression-guard".to_owned()),
5334            peer_pid: None,
5335            peer_session_ulid: None,
5336        })
5337        .into();
5338        state
5339            .dispatch(&req)
5340            .expect("SetMetricDetail must traverse dispatch without UndispatchableRequest");
5341    }
5342}