Skip to main content

sozu_command_lib/proto/
mod.rs

1use std::collections::BTreeMap;
2
3use command::{
4    AggregatedMetrics, BackendMetrics, Bucket, FilteredHistogram, FilteredMetrics, Percentiles,
5    filtered_metrics::Inner,
6};
7use prost::UnknownEnumValue;
8
9/// Contains all types received by and sent from Sōzu
10pub mod command {
11    include!(concat!(env!("OUT_DIR"), "/command.rs"));
12
13    /// Hand-written in `crate::secret` so that decoding wipes a secret before
14    /// its buffer can be released; `command/build.rs` maps the message to it.
15    pub use crate::secret::HealthCheckHeaderValue;
16}
17
18/// Implementation of fmt::Display for the protobuf types, used in the CLI
19pub mod display;
20
21#[derive(thiserror::Error, Debug)]
22pub enum DisplayError {
23    #[error("Could not display content")]
24    DisplayContent(String),
25    #[error("Error while parsing response to JSON")]
26    Json(serde_json::Error),
27    #[error("got the wrong response content type: {0}")]
28    WrongResponseType(String),
29    #[error("Could not format the datetime to ISO 8601")]
30    DateTime,
31    #[error("unrecognized protobuf variant: {0}")]
32    DecodeError(UnknownEnumValue),
33}
34
35// Simple helper to build ResponseContent from ContentType
36impl From<command::response_content::ContentType> for command::ResponseContent {
37    fn from(value: command::response_content::ContentType) -> Self {
38        Self {
39            content_type: Some(value),
40        }
41    }
42}
43
44// Simple helper to build Request from RequestType
45impl From<command::request::RequestType> for command::Request {
46    fn from(value: command::request::RequestType) -> Self {
47        Self {
48            request_type: Some(value),
49        }
50    }
51}
52
53impl std::fmt::Debug for command::Request {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let mut request = f.debug_struct("Request");
56        match self.request_type.as_ref() {
57            Some(request_type) => request.field("request_type", request_type),
58            None => request.field("request_type", &"Unallowed"),
59        };
60        request.finish()
61    }
62}
63
64impl std::fmt::Debug for command::request::RequestType {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        use command::request::RequestType;
67
68        match self {
69            RequestType::AddCluster(value) => f.debug_tuple("AddCluster").field(value).finish(),
70            RequestType::AddHttpFrontend(value) => {
71                f.debug_tuple("AddHttpFrontend").field(value).finish()
72            }
73            RequestType::AddHttpFrontendWithSilentClose(value) => f
74                .debug_tuple("AddHttpFrontendWithSilentClose")
75                .field(value)
76                .finish(),
77            RequestType::AddHttpsFrontendWithSilentClose(value) => f
78                .debug_tuple("AddHttpsFrontendWithSilentClose")
79                .field(value)
80                .finish(),
81            RequestType::RemoveHttpFrontend(value) => {
82                f.debug_tuple("RemoveHttpFrontend").field(value).finish()
83            }
84            RequestType::AddHttpsFrontend(value) => {
85                f.debug_tuple("AddHttpsFrontend").field(value).finish()
86            }
87            RequestType::RemoveHttpsFrontend(value) => {
88                f.debug_tuple("RemoveHttpsFrontend").field(value).finish()
89            }
90            RequestType::AddHttpFrontendWithHeaderPredicates(value) => f
91                .debug_tuple("AddHttpFrontendWithHeaderPredicates")
92                .field(value)
93                .finish(),
94            RequestType::RemoveHttpFrontendWithHeaderPredicates(value) => f
95                .debug_tuple("RemoveHttpFrontendWithHeaderPredicates")
96                .field(value)
97                .finish(),
98            RequestType::AddHttpsFrontendWithHeaderPredicates(value) => f
99                .debug_tuple("AddHttpsFrontendWithHeaderPredicates")
100                .field(value)
101                .finish(),
102            RequestType::RemoveHttpsFrontendWithHeaderPredicates(value) => f
103                .debug_tuple("RemoveHttpsFrontendWithHeaderPredicates")
104                .field(value)
105                .finish(),
106            RequestType::AddCertificate(value) => {
107                f.debug_tuple("AddCertificate").field(value).finish()
108            }
109            RequestType::ReplaceCertificate(value) => {
110                f.debug_tuple("ReplaceCertificate").field(value).finish()
111            }
112            RequestType::RemoveCertificate(value) => {
113                f.debug_tuple("RemoveCertificate").field(value).finish()
114            }
115            RequestType::AddTcpFrontend(value) => {
116                f.debug_tuple("AddTcpFrontend").field(value).finish()
117            }
118            RequestType::RemoveTcpFrontend(value) => {
119                f.debug_tuple("RemoveTcpFrontend").field(value).finish()
120            }
121            RequestType::AddHttpListener(value) => {
122                f.debug_tuple("AddHttpListener").field(value).finish()
123            }
124            RequestType::AddHttpsListener(value) => {
125                f.debug_tuple("AddHttpsListener").field(value).finish()
126            }
127            RequestType::AddHttpsListenerWithClientAuth(value) => f
128                .debug_tuple("AddHttpsListenerWithClientAuth")
129                .field(value)
130                .finish(),
131            RequestType::UpdateHttpListener(value) => {
132                f.debug_tuple("UpdateHttpListener").field(value).finish()
133            }
134            RequestType::UpdateHttpsListener(value) => {
135                f.debug_tuple("UpdateHttpsListener").field(value).finish()
136            }
137            RequestType::UpdateHttpsListenerWithClientAuth(value) => f
138                .debug_tuple("UpdateHttpsListenerWithClientAuth")
139                .field(value)
140                .finish(),
141            RequestType::QueryCertificatesFromTheState(value) => f
142                .debug_tuple("QueryCertificatesFromTheState")
143                .field(value)
144                .finish(),
145            RequestType::QueryCertificatesFromWorkers(value) => f
146                .debug_tuple("QueryCertificatesFromWorkers")
147                .field(value)
148                .finish(),
149            other => f.write_str(display::format_request_type(other)),
150        }
151    }
152}
153
154impl std::fmt::Debug for command::CertificateAndKey {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        let certificate_chain_len = self
157            .certificate_chain
158            .iter()
159            .map(String::len)
160            .fold(0usize, usize::saturating_add);
161
162        f.debug_struct("CertificateAndKey")
163            .field("certificate", &"[redacted]")
164            .field("certificate_len", &self.certificate.len())
165            .field("certificate_chain", &"[redacted]")
166            .field("certificate_chain_count", &self.certificate_chain.len())
167            .field("certificate_chain_len", &certificate_chain_len)
168            .field("key", &"[redacted]")
169            .field("key_len", &self.key.len())
170            .field("versions_count", &self.versions.len())
171            .field("names_count", &self.names.len())
172            .field("names_len", &total_string_len(&self.names))
173            .finish()
174    }
175}
176
177impl std::fmt::Debug for command::RemoveCertificate {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("RemoveCertificate")
180            .field("address", &self.address)
181            .field("fingerprint_len", &self.fingerprint.len())
182            .finish_non_exhaustive()
183    }
184}
185
186impl std::fmt::Debug for command::ReplaceCertificate {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        f.debug_struct("ReplaceCertificate")
189            .field("address", &self.address)
190            .field("new_certificate", &self.new_certificate)
191            .field("fingerprint_len", &self.old_fingerprint.len())
192            .field("new_expired_at", &self.new_expired_at)
193            .finish_non_exhaustive()
194    }
195}
196
197impl std::fmt::Debug for command::Cluster {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        let (answers_count, answers_key_len, answers_value_len) =
200            summarize_string_map(&self.answers);
201
202        f.debug_struct("Cluster")
203            .field("cluster_id_len", &self.cluster_id.len())
204            .field("sticky_session", &self.sticky_session)
205            .field("https_redirect", &self.https_redirect)
206            .field("proxy_protocol", &self.proxy_protocol)
207            .field("load_balancing", &self.load_balancing)
208            .field("answer_503_len", &self.answer_503.as_ref().map(String::len))
209            .field("load_metric", &self.load_metric)
210            .field("http2", &self.http2)
211            .field("answers_count", &answers_count)
212            .field("answers_key_len", &answers_key_len)
213            .field("answers_value_len", &answers_value_len)
214            .field("https_redirect_port", &self.https_redirect_port)
215            .field("authorized_hashes_count", &self.authorized_hashes.len())
216            .field(
217                "authorized_hashes_len",
218                &total_string_len(&self.authorized_hashes),
219            )
220            .field(
221                "www_authenticate_len",
222                &self.www_authenticate.as_ref().map(String::len),
223            )
224            .field("max_connections_per_ip", &self.max_connections_per_ip)
225            .field("retry_after", &self.retry_after)
226            .field("health_check_present", &self.health_check.is_some())
227            .field("udp_present", &self.udp.is_some())
228            .field(
229                "affinity_header_len",
230                &self.affinity_header.as_ref().map(String::len),
231            )
232            .field(
233                "affinity_cookie_len",
234                &self.affinity_cookie.as_ref().map(String::len),
235            )
236            .field("shard_percent", &self.shard_percent)
237            .field("shard_min_backends", &self.shard_min_backends)
238            .field("shard_mode", &self.shard_mode)
239            .finish_non_exhaustive()
240    }
241}
242
243impl std::fmt::Debug for command::Header {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        f.debug_struct("Header")
246            .field("position", &self.position)
247            .field("key_len", &self.key.len())
248            .field("val_len", &self.val.len())
249            .finish_non_exhaustive()
250    }
251}
252
253/// Longest header name, in bytes, [`command::HealthCheckHeader`]'s `Debug` prints.
254const HEALTH_CHECK_HEADER_DEBUG_NAME_MAX: usize = 64;
255
256/// `name` cut to [`HEALTH_CHECK_HEADER_DEBUG_NAME_MAX`] bytes on a char
257/// boundary, and whether it was cut.
258fn bounded_health_check_header_name(name: &str) -> (&str, bool) {
259    let mut end = name.len().min(HEALTH_CHECK_HEADER_DEBUG_NAME_MAX);
260    while !name.is_char_boundary(end) {
261        end -= 1;
262    }
263    (&name[..end], end < name.len())
264}
265
266/// A health-check header is public metadata: `Debug` prints a bounded name and
267/// the keyed fingerprint of the value, never the value, which this type does
268/// not carry.
269impl std::fmt::Debug for command::HealthCheckHeader {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        let (name, truncated) = bounded_health_check_header_name(&self.name);
272        f.debug_struct("HealthCheckHeader")
273            .field("name", &name)
274            .field("name_truncated", &truncated)
275            .field("fingerprint", &self.fingerprint)
276            .finish()
277    }
278}
279
280/// The ingress value is a secret: `Debug` prints a bounded name and a fixed
281/// placeholder, never the bytes and never their length.
282impl std::fmt::Debug for command::HealthCheckHeaderValue {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let (name, truncated) = bounded_health_check_header_name(&self.name);
285        f.debug_struct("HealthCheckHeaderValue")
286            .field("name", &name)
287            .field("name_truncated", &truncated)
288            .field("value", &format_args!("<protected>"))
289            .finish()
290    }
291}
292
293impl std::fmt::Debug for command::HeaderPredicate {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.debug_struct("HeaderPredicate")
296            .field("kind", &self.kind)
297            .field("name_len", &self.name.len())
298            .field("value_len", &self.value.as_ref().map(String::len))
299            .finish_non_exhaustive()
300    }
301}
302
303impl std::fmt::Debug for command::RequestHttpFrontend {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        let tags_len = self.tags.iter().fold(0usize, |total, (key, value)| {
306            total.saturating_add(key.len()).saturating_add(value.len())
307        });
308        let headers_len = self.headers.iter().fold(0usize, |total, header| {
309            total
310                .saturating_add(header.key.len())
311                .saturating_add(header.val.len())
312        });
313
314        f.debug_struct("RequestHttpFrontend")
315            .field("cluster_id_len", &self.cluster_id.as_ref().map(String::len))
316            .field("address", &self.address)
317            .field("hostname_len", &self.hostname.len())
318            .field("path_kind", &self.path.kind)
319            .field("path_len", &self.path.value.len())
320            .field("method_len", &self.method.as_ref().map(String::len))
321            .field("position", &self.position)
322            .field("tags_count", &self.tags.len())
323            .field("tags_len", &tags_len)
324            .field("redirect", &self.redirect)
325            .field("required_auth", &self.required_auth)
326            .field("redirect_scheme", &self.redirect_scheme)
327            .field(
328                "redirect_template_len",
329                &self.redirect_template.as_ref().map(String::len),
330            )
331            .field(
332                "rewrite_host_len",
333                &self.rewrite_host.as_ref().map(String::len),
334            )
335            .field(
336                "rewrite_path_len",
337                &self.rewrite_path.as_ref().map(String::len),
338            )
339            .field("rewrite_port", &self.rewrite_port)
340            .field("headers_count", &self.headers.len())
341            .field("headers_len", &headers_len)
342            .field("hsts", &self.hsts)
343            .field("header_predicates_count", &self.header_predicates.len())
344            .finish_non_exhaustive()
345    }
346}
347
348impl std::fmt::Debug for command::RequestTcpFrontend {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        let (tags_count, tags_key_len, tags_value_len) = summarize_string_map(&self.tags);
351
352        f.debug_struct("RequestTcpFrontend")
353            .field("cluster_id_len", &self.cluster_id.len())
354            .field("address", &self.address)
355            .field("tags_count", &tags_count)
356            .field("tags_key_len", &tags_key_len)
357            .field("tags_value_len", &tags_value_len)
358            .field("sni_len", &self.sni.as_ref().map(String::len))
359            .field("alpn_count", &self.alpn.len())
360            .field("alpn_len", &total_string_len(&self.alpn))
361            .finish_non_exhaustive()
362    }
363}
364
365fn total_string_len<'a>(values: impl IntoIterator<Item = &'a String>) -> usize {
366    values
367        .into_iter()
368        .map(String::len)
369        .fold(0usize, usize::saturating_add)
370}
371
372fn summarize_http_answers(
373    answers: Option<&command::CustomHttpAnswers>,
374) -> (Option<&'static str>, Option<usize>, Option<usize>) {
375    let Some(answers) = answers else {
376        return (None, None, None);
377    };
378    let values = [
379        answers.answer_301.as_ref(),
380        answers.answer_400.as_ref(),
381        answers.answer_401.as_ref(),
382        answers.answer_404.as_ref(),
383        answers.answer_408.as_ref(),
384        answers.answer_413.as_ref(),
385        answers.answer_421.as_ref(),
386        answers.answer_429.as_ref(),
387        answers.answer_502.as_ref(),
388        answers.answer_503.as_ref(),
389        answers.answer_504.as_ref(),
390        answers.answer_507.as_ref(),
391    ];
392
393    (
394        Some("[redacted]"),
395        Some(values.iter().flatten().count()),
396        Some(total_string_len(values.iter().flatten().copied())),
397    )
398}
399
400fn summarize_string_map(
401    values: &std::collections::BTreeMap<String, String>,
402) -> (usize, usize, usize) {
403    (
404        values.len(),
405        total_string_len(values.keys()),
406        total_string_len(values.values()),
407    )
408}
409
410impl std::fmt::Debug for command::HttpListenerConfig {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        let (http_answers, http_answers_count, http_answers_len) =
413            summarize_http_answers(self.http_answers.as_ref());
414        let (answers_count, answers_key_len, answers_value_len) =
415            summarize_string_map(&self.answers);
416
417        f.debug_struct("HttpListenerConfig")
418            .field("address", &self.address)
419            .field("interface", &self.interface)
420            .field("public_address", &self.public_address)
421            .field("expect_proxy", &self.expect_proxy)
422            .field("sticky_name_len", &self.sticky_name.len())
423            .field("front_timeout", &self.front_timeout)
424            .field("back_timeout", &self.back_timeout)
425            .field("connect_timeout", &self.connect_timeout)
426            .field("request_timeout", &self.request_timeout)
427            .field("active", &self.active)
428            .field("http_answers", &http_answers)
429            .field("http_answers_count", &http_answers_count)
430            .field("http_answers_len", &http_answers_len)
431            .field(
432                "sozu_id_header_len",
433                &self.sozu_id_header.as_ref().map(String::len),
434            )
435            .field("answers_count", &answers_count)
436            .field("answers_key_len", &answers_key_len)
437            .field("answers_value_len", &answers_value_len)
438            .field("elide_x_real_ip", &self.elide_x_real_ip)
439            .field("send_x_real_ip", &self.send_x_real_ip)
440            .field("forwarded_headers", &self.forwarded_headers)
441            .field("h2c_prior_knowledge", &self.h2c_prior_knowledge)
442            .finish_non_exhaustive()
443    }
444}
445
446impl std::fmt::Debug for command::UpdateHttpListenerConfig {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        let (http_answers, http_answers_count, http_answers_len) =
449            summarize_http_answers(self.http_answers.as_ref());
450        let (answers_count, answers_key_len, answers_value_len) =
451            summarize_string_map(&self.answers);
452
453        f.debug_struct("UpdateHttpListenerConfig")
454            .field("address", &self.address)
455            .field("public_address", &self.public_address)
456            .field(
457                "sticky_name_len",
458                &self.sticky_name.as_ref().map(String::len),
459            )
460            .field("http_answers", &http_answers)
461            .field("http_answers_count", &http_answers_count)
462            .field("http_answers_len", &http_answers_len)
463            .field(
464                "sozu_id_header_len",
465                &self.sozu_id_header.as_ref().map(String::len),
466            )
467            .field("answers_count", &answers_count)
468            .field("answers_key_len", &answers_key_len)
469            .field("answers_value_len", &answers_value_len)
470            .field("forwarded_headers", &self.forwarded_headers)
471            .field("h2c_prior_knowledge", &self.h2c_prior_knowledge)
472            .finish_non_exhaustive()
473    }
474}
475
476impl std::fmt::Debug for command::UpdateHttpsListenerConfig {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        let (http_answers, http_answers_count, http_answers_len) =
479            summarize_http_answers(self.http_answers.as_ref());
480        let alpn_protocols_count = self
481            .alpn_protocols
482            .as_ref()
483            .map(|protocols| protocols.values.len());
484        let alpn_protocols_len = self
485            .alpn_protocols
486            .as_ref()
487            .map(|protocols| total_string_len(&protocols.values));
488        let (answers_count, answers_key_len, answers_value_len) =
489            summarize_string_map(&self.answers);
490
491        f.debug_struct("UpdateHttpsListenerConfig")
492            .field("address", &self.address)
493            .field("public_address", &self.public_address)
494            .field(
495                "sticky_name_len",
496                &self.sticky_name.as_ref().map(String::len),
497            )
498            .field("http_answers", &http_answers)
499            .field("http_answers_count", &http_answers_count)
500            .field("http_answers_len", &http_answers_len)
501            .field("alpn_protocols_count", &alpn_protocols_count)
502            .field("alpn_protocols_len", &alpn_protocols_len)
503            .field(
504                "sozu_id_header_len",
505                &self.sozu_id_header.as_ref().map(String::len),
506            )
507            .field("answers_count", &answers_count)
508            .field("answers_key_len", &answers_key_len)
509            .field("answers_value_len", &answers_value_len)
510            .field("forwarded_headers", &self.forwarded_headers)
511            .field("client_auth_policy", &self.client_auth_policy)
512            .finish_non_exhaustive()
513    }
514}
515
516/// CA and CRL entries are PEM bodies: counted and measured, never printed.
517impl std::fmt::Debug for command::ClientAuthPolicy {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        f.debug_struct("ClientAuthPolicy")
520            .field("client_auth", &self.client_auth)
521            .field(
522                "client_ca_certificates_count",
523                &self.client_ca_certificates.len(),
524            )
525            .field(
526                "client_ca_certificates_len",
527                &total_string_len(&self.client_ca_certificates),
528            )
529            .field("client_ca_crls_count", &self.client_ca_crls.len())
530            .field(
531                "client_ca_crls_len",
532                &total_string_len(&self.client_ca_crls),
533            )
534            .finish()
535    }
536}
537
538impl std::fmt::Debug for command::HttpsListenerConfig {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        let certificate = self.certificate.as_ref().map(|_| "[redacted]");
541        let certificate_len = self.certificate.as_ref().map(String::len);
542        let certificate_chain_len = total_string_len(&self.certificate_chain);
543        let key = self.key.as_ref().map(|_| "[redacted]");
544        let key_len = self.key.as_ref().map(String::len);
545        let (http_answers, http_answers_count, http_answers_len) =
546            summarize_http_answers(self.http_answers.as_ref());
547        let sozu_id_header_len = self.sozu_id_header.as_ref().map(String::len);
548        let (answers_count, answers_key_len, answers_value_len) =
549            summarize_string_map(&self.answers);
550
551        f.debug_struct("HttpsListenerConfig")
552            .field("address", &self.address)
553            .field("interface", &self.interface)
554            .field("public_address", &self.public_address)
555            .field("expect_proxy", &self.expect_proxy)
556            .field("sticky_name_len", &self.sticky_name.len())
557            .field("front_timeout", &self.front_timeout)
558            .field("back_timeout", &self.back_timeout)
559            .field("connect_timeout", &self.connect_timeout)
560            .field("request_timeout", &self.request_timeout)
561            .field("active", &self.active)
562            .field("versions_count", &self.versions.len())
563            .field("cipher_list_count", &self.cipher_list.len())
564            .field("cipher_list_len", &total_string_len(&self.cipher_list))
565            .field("cipher_suites_count", &self.cipher_suites.len())
566            .field("cipher_suites_len", &total_string_len(&self.cipher_suites))
567            .field(
568                "signature_algorithms_count",
569                &self.signature_algorithms.len(),
570            )
571            .field(
572                "signature_algorithms_len",
573                &total_string_len(&self.signature_algorithms),
574            )
575            .field("groups_list_count", &self.groups_list.len())
576            .field("groups_list_len", &total_string_len(&self.groups_list))
577            .field("certificate", &certificate)
578            .field("certificate_len", &certificate_len)
579            .field("certificate_chain", &"[redacted]")
580            .field("certificate_chain_count", &self.certificate_chain.len())
581            .field("certificate_chain_len", &certificate_chain_len)
582            .field("key", &key)
583            .field("key_len", &key_len)
584            .field("send_tls13_tickets", &self.send_tls13_tickets)
585            .field("http_answers", &http_answers)
586            .field("http_answers_count", &http_answers_count)
587            .field("http_answers_len", &http_answers_len)
588            .field("alpn_protocols_count", &self.alpn_protocols.len())
589            .field(
590                "alpn_protocols_len",
591                &total_string_len(&self.alpn_protocols),
592            )
593            .field(
594                "h2_max_rst_stream_per_window",
595                &self.h2_max_rst_stream_per_window,
596            )
597            .field("h2_max_ping_per_window", &self.h2_max_ping_per_window)
598            .field(
599                "h2_max_settings_per_window",
600                &self.h2_max_settings_per_window,
601            )
602            .field(
603                "h2_max_empty_data_per_window",
604                &self.h2_max_empty_data_per_window,
605            )
606            .field(
607                "h2_max_continuation_frames",
608                &self.h2_max_continuation_frames,
609            )
610            .field("h2_max_glitch_count", &self.h2_max_glitch_count)
611            .field(
612                "h2_initial_connection_window",
613                &self.h2_initial_connection_window,
614            )
615            .field("h2_max_concurrent_streams", &self.h2_max_concurrent_streams)
616            .field("h2_stream_shrink_ratio", &self.h2_stream_shrink_ratio)
617            .field(
618                "h2_max_rst_stream_lifetime",
619                &self.h2_max_rst_stream_lifetime,
620            )
621            .field(
622                "h2_max_rst_stream_abusive_lifetime",
623                &self.h2_max_rst_stream_abusive_lifetime,
624            )
625            .field(
626                "h2_max_rst_stream_emitted_lifetime",
627                &self.h2_max_rst_stream_emitted_lifetime,
628            )
629            .field("h2_max_header_list_size", &self.h2_max_header_list_size)
630            .field("strict_sni_binding", &self.strict_sni_binding)
631            .field("disable_http11", &self.disable_http11)
632            .field(
633                "h2_stream_idle_timeout_seconds",
634                &self.h2_stream_idle_timeout_seconds,
635            )
636            .field("h2_max_header_table_size", &self.h2_max_header_table_size)
637            .field(
638                "h2_graceful_shutdown_deadline_seconds",
639                &self.h2_graceful_shutdown_deadline_seconds,
640            )
641            .field(
642                "h2_max_window_update_stream0_per_window",
643                &self.h2_max_window_update_stream0_per_window,
644            )
645            .field("sozu_id_header_len", &sozu_id_header_len)
646            .field("answers_count", &answers_count)
647            .field("answers_key_len", &answers_key_len)
648            .field("answers_value_len", &answers_value_len)
649            .field("elide_x_real_ip", &self.elide_x_real_ip)
650            .field("send_x_real_ip", &self.send_x_real_ip)
651            .field("forwarded_headers", &self.forwarded_headers)
652            .field("hsts", &self.hsts)
653            .field("h2_max_header_fields", &self.h2_max_header_fields)
654            .field("h2_stream_refusal_percent", &self.h2_stream_refusal_percent)
655            .field("client_auth", &self.client_auth)
656            .field(
657                "client_ca_certificates_count",
658                &self.client_ca_certificates.len(),
659            )
660            .field(
661                "client_ca_certificates_len",
662                &total_string_len(&self.client_ca_certificates),
663            )
664            .field("client_ca_crls_count", &self.client_ca_crls.len())
665            .field(
666                "client_ca_crls_len",
667                &total_string_len(&self.client_ca_crls),
668            )
669            .finish()
670    }
671}
672
673impl std::fmt::Debug for command::QueryCertificatesFilters {
674    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
675        f.debug_struct("QueryCertificatesFilters")
676            .field("domain_len", &self.domain.as_ref().map(String::len))
677            .field(
678                "fingerprint_len",
679                &self.fingerprint.as_ref().map(String::len),
680            )
681            .finish()
682    }
683}
684
685impl std::fmt::Debug for command::CertificateSummary {
686    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687        f.debug_struct("CertificateSummary")
688            .field("domain_len", &self.domain.len())
689            .field("fingerprint_len", &self.fingerprint.len())
690            .finish()
691    }
692}
693
694impl std::fmt::Debug for command::CertificatesByAddress {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        let domain_len = self
697            .certificate_summaries
698            .iter()
699            .map(|summary| summary.domain.len())
700            .fold(0usize, usize::saturating_add);
701        let fingerprint_len = self
702            .certificate_summaries
703            .iter()
704            .map(|summary| summary.fingerprint.len())
705            .fold(0usize, usize::saturating_add);
706
707        f.debug_struct("CertificatesByAddress")
708            .field("address", &self.address)
709            .field("certificates_count", &self.certificate_summaries.len())
710            .field("domain_len", &domain_len)
711            .field("fingerprint_len", &fingerprint_len)
712            .finish()
713    }
714}
715
716impl std::fmt::Debug for command::ListOfCertificatesByAddress {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        let certificates_count = self
719            .certificates
720            .iter()
721            .map(|entry| entry.certificate_summaries.len())
722            .fold(0usize, usize::saturating_add);
723        let domain_len = self
724            .certificates
725            .iter()
726            .flat_map(|entry| &entry.certificate_summaries)
727            .map(|summary| summary.domain.len())
728            .fold(0usize, usize::saturating_add);
729        let fingerprint_len = self
730            .certificates
731            .iter()
732            .flat_map(|entry| &entry.certificate_summaries)
733            .map(|summary| summary.fingerprint.len())
734            .fold(0usize, usize::saturating_add);
735
736        f.debug_struct("ListOfCertificatesByAddress")
737            .field("listeners_count", &self.certificates.len())
738            .field("certificates_count", &certificates_count)
739            .field("domain_len", &domain_len)
740            .field("fingerprint_len", &fingerprint_len)
741            .finish()
742    }
743}
744
745impl std::fmt::Debug for command::CertificatesWithFingerprints {
746    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
747        let fingerprint_len = total_string_len(self.certs.keys());
748        let names_count = self
749            .certs
750            .values()
751            .map(|certificate| certificate.names.len())
752            .fold(0usize, usize::saturating_add);
753        let names_len = self
754            .certs
755            .values()
756            .flat_map(|certificate| &certificate.names)
757            .map(String::len)
758            .fold(0usize, usize::saturating_add);
759
760        f.debug_struct("CertificatesWithFingerprints")
761            .field("certificates_count", &self.certs.len())
762            .field("fingerprint_len", &fingerprint_len)
763            .field("names_count", &names_count)
764            .field("names_len", &names_len)
765            .finish()
766    }
767}
768
769fn response_content_kind(content: Option<&command::ResponseContent>) -> Option<&'static str> {
770    use command::response_content::ContentType;
771
772    content
773        .and_then(|response| response.content_type.as_ref())
774        .map(|content_type| match content_type {
775            ContentType::Workers(_) => "workers",
776            ContentType::Metrics(_) => "metrics",
777            ContentType::WorkerResponses(_) => "worker_responses",
778            ContentType::Event(_) => "event",
779            ContentType::FrontendList(_) => "frontend_list",
780            ContentType::ListenersList(_) => "listeners_list",
781            ContentType::WorkerMetrics(_) => "worker_metrics",
782            ContentType::AvailableMetrics(_) => "available_metrics",
783            ContentType::Clusters(_) => "clusters",
784            ContentType::ClusterHashes(_) => "cluster_hashes",
785            ContentType::CertificatesByAddress(_) => "certificates_by_address",
786            ContentType::CertificatesWithFingerprints(_) => "certificates_with_fingerprints",
787            ContentType::RequestCounts(_) => "request_counts",
788            ContentType::MaxConnectionsPerIpLimit(_) => "max_connections_per_ip_limit",
789            ContentType::MaxConnectionsPerSubnetLimit(_) => "max_connections_per_subnet_limit",
790            ContentType::HealthChecksList(_) => "health_checks_list",
791            ContentType::MetricDetailStatus(_) => "metric_detail_status",
792            ContentType::WorkerMetricDetailStatus(_) => "worker_metric_detail_status",
793            ContentType::ClosedSessions(_) => "closed_sessions",
794            ContentType::ProtectedHealthCheckReply(_) => "protected_health_check_reply",
795        })
796}
797
798impl std::fmt::Debug for command::ResponseContent {
799    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
800        f.debug_struct("ResponseContent")
801            .field("content_type", &response_content_kind(Some(self)))
802            .finish_non_exhaustive()
803    }
804}
805
806impl std::fmt::Debug for command::WorkerResponses {
807    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808        f.debug_struct("WorkerResponses")
809            .field("workers_count", &self.map.len())
810            .field("worker_ids_len", &total_string_len(self.map.keys()))
811            .finish_non_exhaustive()
812    }
813}
814
815impl std::fmt::Debug for command::WorkerRequest {
816    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817        f.debug_struct("WorkerRequest")
818            .field("id_len", &self.id.len())
819            .field("content", &self.content)
820            .finish()
821    }
822}
823
824impl std::fmt::Debug for command::WorkerResponse {
825    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826        f.debug_struct("WorkerResponse")
827            .field("id_len", &self.id.len())
828            .field("status", &self.status)
829            .field("message_len", &self.message.len())
830            .field(
831                "content_type",
832                &response_content_kind(self.content.as_ref()),
833            )
834            .finish()
835    }
836}
837
838impl AggregatedMetrics {
839    /// Merge metrics that were received from several workers
840    ///
841    /// Each worker gather the same kind of metrics,
842    /// for its own proxying logic, and for the same clusters with their backends.
843    /// This means we have to reduce each metric from N instances to 1.
844    pub fn merge_metrics(&mut self) {
845        // avoid copying the worker metrics, by taking them
846        let workers = std::mem::take(&mut self.workers);
847
848        for (_worker_id, worker) in workers {
849            for (metric_name, new_value) in worker.proxy {
850                if new_value.is_mergeable() {
851                    self.proxying
852                        .entry(metric_name)
853                        .and_modify(|old_value| old_value.merge(&new_value))
854                        .or_insert(new_value);
855                }
856            }
857
858            for (cluster_id, mut cluster_metrics) in worker.clusters {
859                for (metric_name, new_value) in cluster_metrics.cluster {
860                    if new_value.is_mergeable() {
861                        let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
862
863                        cluster
864                            .cluster
865                            .entry(metric_name)
866                            .and_modify(|old_value| old_value.merge(&new_value))
867                            .or_insert(new_value);
868                    }
869                }
870
871                for backend in cluster_metrics.backends.drain(..) {
872                    for (metric_name, new_value) in backend.metrics {
873                        if new_value.is_mergeable() {
874                            let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
875
876                            let found_backend = cluster
877                                .backends
878                                .iter_mut()
879                                .find(|present| present.backend_id == backend.backend_id);
880
881                            if let Some(existing_backend) = found_backend {
882                                let _ = existing_backend
883                                    .metrics
884                                    .entry(metric_name)
885                                    .and_modify(|old_value| old_value.merge(&new_value))
886                                    .or_insert(new_value);
887                            } else {
888                                cluster.backends.push(BackendMetrics {
889                                    backend_id: backend.backend_id.clone(),
890                                    metrics: BTreeMap::from([(metric_name, new_value)]),
891                                });
892                            };
893                        }
894                    }
895                }
896            }
897        }
898    }
899}
900
901impl FilteredMetrics {
902    pub fn merge(&mut self, right: &Self) {
903        match (&self.inner, &right.inner) {
904            (Some(Inner::Gauge(a)), Some(Inner::Gauge(b))) => {
905                *self = Self {
906                    inner: Some(Inner::Gauge(a + b)),
907                };
908            }
909            (Some(Inner::Count(a)), Some(Inner::Count(b))) => {
910                *self = Self {
911                    inner: Some(Inner::Count(a + b)),
912                };
913            }
914            (Some(Inner::Histogram(a)), Some(Inner::Histogram(b))) => {
915                let longest_len = a.buckets.len().max(b.buckets.len());
916
917                let mut a_count = 0;
918                let mut b_count = 0;
919                let buckets = (0..longest_len)
920                    .map(|i| {
921                        if let Some(a_bucket) = a.buckets.get(i) {
922                            a_count = a_bucket.count;
923                        }
924                        if let Some(b_bucket) = b.buckets.get(i) {
925                            b_count = b_bucket.count;
926                        }
927                        Bucket {
928                            le: (1 << i) - 1, // the bucket less-or-equal limits are normalized: 0, 1, 3, 7, 15, ...
929                            count: a_count + b_count,
930                        }
931                    })
932                    .collect();
933
934                *self = Self {
935                    inner: Some(Inner::Histogram(FilteredHistogram {
936                        count: a.count + b.count,
937                        sum: a.sum + b.sum,
938                        buckets,
939                    })),
940                };
941            }
942            (Some(Inner::Percentiles(a)), Some(Inner::Percentiles(b))) => {
943                // You cannot statistically merge two percentile summaries
944                // without the underlying samples. The companion
945                // `<name>_histogram` Inner::Histogram value is the source
946                // of truth for accurate aggregation and merges correctly
947                // above. We still propagate the percentile shape so legacy
948                // consumers reading it observe at least the worst-case
949                // upper bound across workers — element-wise max preserves
950                // the "is anyone slow?" intent. `samples` and `sum` add so
951                // the totals reflect cross-worker volume.
952                *self = Self {
953                    inner: Some(Inner::Percentiles(Percentiles {
954                        samples: a.samples + b.samples,
955                        p_50: a.p_50.max(b.p_50),
956                        p_90: a.p_90.max(b.p_90),
957                        p_99: a.p_99.max(b.p_99),
958                        p_99_9: a.p_99_9.max(b.p_99_9),
959                        p_99_99: a.p_99_99.max(b.p_99_99),
960                        p_99_999: a.p_99_999.max(b.p_99_999),
961                        p_100: a.p_100.max(b.p_100),
962                        sum: a.sum + b.sum,
963                    })),
964                };
965            }
966            _ => {}
967        }
968    }
969
970    fn is_mergeable(&self) -> bool {
971        match &self.inner {
972            Some(Inner::Gauge(_))
973            | Some(Inner::Count(_))
974            | Some(Inner::Histogram(_))
975            | Some(Inner::Percentiles(_)) => true,
976            // Inner::Time and Inner::Timeserie are never used in Sōzu
977            Some(Inner::Time(_)) | Some(Inner::TimeSerie(_)) | None => false,
978        }
979    }
980}
981
982#[cfg(test)]
983mod tests {
984    use std::{
985        collections::{BTreeMap, HashMap},
986        net::SocketAddr,
987    };
988
989    use crate::{certificate::Fingerprint, state::ConfigState};
990
991    use super::AggregatedMetrics;
992    use super::command::{
993        AddCertificate, AlpnProtocols, Bucket, CertificateAndKey, CertificateSummary,
994        CertificatesByAddress, CertificatesWithFingerprints, Cluster, ClusterMetrics,
995        CustomHttpAnswers, FilteredHistogram, FilteredMetrics, Header, HeaderMatchKind,
996        HeaderPosition, HeaderPredicate, HttpListenerConfig, HttpsListenerConfig,
997        ListOfCertificatesByAddress, PathRule, PathRuleKind, Percentiles, QueryCertificatesFilters,
998        RedirectPolicy, RedirectScheme, RemoveCertificate, ReplaceCertificate, Request,
999        RequestHttpFrontend, RequestTcpFrontend, ResponseContent, ResponseStatus, RulePosition,
1000        TlsVersion, UpdateHttpListenerConfig, UpdateHttpsListenerConfig, WorkerMetrics,
1001        WorkerRequest, WorkerResponse, WorkerResponses, filtered_metrics::Inner,
1002        request::RequestType, response_content::ContentType,
1003    };
1004
1005    #[test]
1006    fn request_debug_redacts_string_variant_payloads_directly_and_when_nested() {
1007        const PATH_SECRET: &str = "REQUEST_SAVE_STATE_PATH_SECRET_SENTINEL";
1008
1009        let request_type = RequestType::SaveState(format!("{PATH_SECRET}{}", "x".repeat(4096)));
1010        let direct_output = format!("{request_type:?}");
1011        let request = Request::from(request_type);
1012        let worker_request = WorkerRequest {
1013            id: "safe-request-id".to_owned(),
1014            content: request.clone(),
1015        };
1016        let outputs = [
1017            direct_output,
1018            format!("{request:?}"),
1019            format!("{worker_request:?}"),
1020        ];
1021
1022        for output in outputs {
1023            assert!(
1024                !output.contains(PATH_SECRET),
1025                "Request Debug leaked its string variant payload: {output}"
1026            );
1027            assert!(
1028                output.contains("SaveState"),
1029                "Request Debug omitted the bounded request kind: {output}"
1030            );
1031            assert!(
1032                output.len() <= 256,
1033                "Request Debug output is not bounded: {} bytes",
1034                output.len()
1035            );
1036        }
1037    }
1038
1039    #[test]
1040    fn listener_and_patch_debug_redacts_user_controlled_material() {
1041        const HTTP_STICKY_SECRET: &str = "HTTP_LISTENER_STICKY_SECRET_SENTINEL";
1042        const HTTP_LEGACY_ANSWER_SECRET: &str = "HTTP_LISTENER_LEGACY_ANSWER_SECRET_SENTINEL";
1043        const HTTP_HEADER_SECRET: &str = "HTTP_LISTENER_HEADER_SECRET_SENTINEL";
1044        const HTTP_ANSWER_KEY_SECRET: &str = "HTTP_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1045        const HTTP_ANSWER_BODY_SECRET: &str = "HTTP_LISTENER_ANSWER_BODY_SECRET_SENTINEL";
1046        const HTTP_PATCH_STICKY_SECRET: &str = "HTTP_PATCH_STICKY_SECRET_SENTINEL";
1047        const HTTP_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTP_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1048        const HTTP_PATCH_HEADER_SECRET: &str = "HTTP_PATCH_HEADER_SECRET_SENTINEL";
1049        const HTTP_PATCH_ANSWER_KEY_SECRET: &str = "HTTP_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1050        const HTTP_PATCH_ANSWER_BODY_SECRET: &str = "HTTP_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1051        const HTTPS_PATCH_STICKY_SECRET: &str = "HTTPS_PATCH_STICKY_SECRET_SENTINEL";
1052        const HTTPS_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTPS_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1053        const HTTPS_PATCH_ALPN_SECRET: &str = "HTTPS_PATCH_ALPN_SECRET_SENTINEL";
1054        const HTTPS_PATCH_HEADER_SECRET: &str = "HTTPS_PATCH_HEADER_SECRET_SENTINEL";
1055        const HTTPS_PATCH_ANSWER_KEY_SECRET: &str = "HTTPS_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1056        const HTTPS_PATCH_ANSWER_BODY_SECRET: &str = "HTTPS_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1057
1058        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1059        let http_address: SocketAddr = "127.0.0.1:8080"
1060            .parse()
1061            .expect("test HTTP listener address must parse");
1062        let http_listener = HttpListenerConfig {
1063            address: http_address.into(),
1064            sticky_name: long_value(HTTP_STICKY_SECRET),
1065            http_answers: Some(CustomHttpAnswers {
1066                answer_503: Some(long_value(HTTP_LEGACY_ANSWER_SECRET)),
1067                ..Default::default()
1068            }),
1069            sozu_id_header: Some(long_value(HTTP_HEADER_SECRET)),
1070            answers: BTreeMap::from([(
1071                long_value(HTTP_ANSWER_KEY_SECRET),
1072                long_value(HTTP_ANSWER_BODY_SECRET),
1073            )]),
1074            ..Default::default()
1075        };
1076        let http_request = Request::from(RequestType::AddHttpListener(http_listener.clone()));
1077        let http_worker_request = WorkerRequest {
1078            id: "safe-add-http-listener-id".to_owned(),
1079            content: http_request.clone(),
1080        };
1081
1082        let http_patch = UpdateHttpListenerConfig {
1083            address: http_address.into(),
1084            sticky_name: Some(long_value(HTTP_PATCH_STICKY_SECRET)),
1085            http_answers: Some(CustomHttpAnswers {
1086                answer_503: Some(long_value(HTTP_PATCH_LEGACY_ANSWER_SECRET)),
1087                ..Default::default()
1088            }),
1089            sozu_id_header: Some(long_value(HTTP_PATCH_HEADER_SECRET)),
1090            answers: BTreeMap::from([(
1091                long_value(HTTP_PATCH_ANSWER_KEY_SECRET),
1092                long_value(HTTP_PATCH_ANSWER_BODY_SECRET),
1093            )]),
1094            ..Default::default()
1095        };
1096        let http_patch_request = Request::from(RequestType::UpdateHttpListener(http_patch.clone()));
1097        let http_patch_worker_request = WorkerRequest {
1098            id: "safe-update-http-listener-id".to_owned(),
1099            content: http_patch_request.clone(),
1100        };
1101
1102        let https_address: SocketAddr = "127.0.0.1:8443"
1103            .parse()
1104            .expect("test HTTPS listener address must parse");
1105        let https_patch = UpdateHttpsListenerConfig {
1106            address: https_address.into(),
1107            sticky_name: Some(long_value(HTTPS_PATCH_STICKY_SECRET)),
1108            http_answers: Some(CustomHttpAnswers {
1109                answer_503: Some(long_value(HTTPS_PATCH_LEGACY_ANSWER_SECRET)),
1110                ..Default::default()
1111            }),
1112            alpn_protocols: Some(AlpnProtocols {
1113                values: vec![long_value(HTTPS_PATCH_ALPN_SECRET)],
1114            }),
1115            sozu_id_header: Some(long_value(HTTPS_PATCH_HEADER_SECRET)),
1116            answers: BTreeMap::from([(
1117                long_value(HTTPS_PATCH_ANSWER_KEY_SECRET),
1118                long_value(HTTPS_PATCH_ANSWER_BODY_SECRET),
1119            )]),
1120            ..Default::default()
1121        };
1122        let https_patch_request =
1123            Request::from(RequestType::UpdateHttpsListener(https_patch.clone()));
1124        let https_patch_worker_request = WorkerRequest {
1125            id: "safe-update-https-listener-id".to_owned(),
1126            content: https_patch_request.clone(),
1127        };
1128
1129        let debug_outputs = [
1130            ("HttpListenerConfig", format!("{http_listener:?}")),
1131            ("AddHttpListener Request", format!("{http_request:?}")),
1132            (
1133                "AddHttpListener WorkerRequest",
1134                format!("{http_worker_request:?}"),
1135            ),
1136            ("UpdateHttpListenerConfig", format!("{http_patch:?}")),
1137            (
1138                "UpdateHttpListener Request",
1139                format!("{http_patch_request:?}"),
1140            ),
1141            (
1142                "UpdateHttpListener WorkerRequest",
1143                format!("{http_patch_worker_request:?}"),
1144            ),
1145            ("UpdateHttpsListenerConfig", format!("{https_patch:?}")),
1146            (
1147                "UpdateHttpsListener Request",
1148                format!("{https_patch_request:?}"),
1149            ),
1150            (
1151                "UpdateHttpsListener WorkerRequest",
1152                format!("{https_patch_worker_request:?}"),
1153            ),
1154        ];
1155        let secrets = [
1156            HTTP_STICKY_SECRET,
1157            HTTP_LEGACY_ANSWER_SECRET,
1158            HTTP_HEADER_SECRET,
1159            HTTP_ANSWER_KEY_SECRET,
1160            HTTP_ANSWER_BODY_SECRET,
1161            HTTP_PATCH_STICKY_SECRET,
1162            HTTP_PATCH_LEGACY_ANSWER_SECRET,
1163            HTTP_PATCH_HEADER_SECRET,
1164            HTTP_PATCH_ANSWER_KEY_SECRET,
1165            HTTP_PATCH_ANSWER_BODY_SECRET,
1166            HTTPS_PATCH_STICKY_SECRET,
1167            HTTPS_PATCH_LEGACY_ANSWER_SECRET,
1168            HTTPS_PATCH_ALPN_SECRET,
1169            HTTPS_PATCH_HEADER_SECRET,
1170            HTTPS_PATCH_ANSWER_KEY_SECRET,
1171            HTTPS_PATCH_ANSWER_BODY_SECRET,
1172        ];
1173        let mut leaks = Vec::new();
1174        for (label, output) in &debug_outputs {
1175            for secret in secrets {
1176                if output.contains(secret) {
1177                    leaks.push(format!("{label}:{secret}"));
1178                }
1179            }
1180        }
1181        assert!(
1182            leaks.is_empty(),
1183            "listener Debug leaked secret markers through {}",
1184            leaks.join(", ")
1185        );
1186
1187        let expected_metadata = [
1188            (0, "sticky_name_len: 4132"),
1189            (0, "http_answers_count: Some(1)"),
1190            (0, "answers_count: 1"),
1191            (3, "sticky_name_len: Some(4129)"),
1192            (3, "http_answers_count: Some(1)"),
1193            (3, "answers_count: 1"),
1194            (6, "sticky_name_len: Some(4130)"),
1195            (6, "http_answers_count: Some(1)"),
1196            (6, "alpn_protocols_count: Some(1)"),
1197            (6, "answers_count: 1"),
1198        ];
1199        for (output_index, safe_metadata) in expected_metadata {
1200            assert!(
1201                debug_outputs[output_index].1.contains(safe_metadata),
1202                "{} Debug omitted safe metadata {safe_metadata}: {}",
1203                debug_outputs[output_index].0,
1204                debug_outputs[output_index].1,
1205            );
1206        }
1207        for (label, output) in debug_outputs {
1208            assert!(
1209                output.len() <= 4096,
1210                "{label} Debug output is not bounded: {} bytes",
1211                output.len()
1212            );
1213        }
1214    }
1215
1216    #[test]
1217    fn https_listener_debug_redacts_user_controlled_material() {
1218        const STICKY_NAME_SECRET: &str = "HTTPS_LISTENER_STICKY_NAME_SECRET_SENTINEL";
1219        const CIPHER_LIST_SECRET: &str = "HTTPS_LISTENER_CIPHER_LIST_SECRET_SENTINEL";
1220        const CIPHER_SUITES_SECRET: &str = "HTTPS_LISTENER_CIPHER_SUITES_SECRET_SENTINEL";
1221        const SIGNATURE_ALGORITHMS_SECRET: &str =
1222            "HTTPS_LISTENER_SIGNATURE_ALGORITHMS_SECRET_SENTINEL";
1223        const GROUPS_LIST_SECRET: &str = "HTTPS_LISTENER_GROUPS_LIST_SECRET_SENTINEL";
1224        const CERTIFICATE_SECRET: &str = "HTTPS_LISTENER_CERTIFICATE_SECRET_SENTINEL";
1225        const CHAIN_SECRET: &str = "HTTPS_LISTENER_CHAIN_SECRET_SENTINEL";
1226        const KEY_SECRET: &str = "HTTPS_LISTENER_KEY_SECRET_SENTINEL";
1227        const HTTP_ANSWER_SECRET: &str = "HTTPS_LISTENER_HTTP_ANSWER_SECRET_SENTINEL";
1228        const ALPN_SECRET: &str = "HTTPS_LISTENER_ALPN_SECRET_SENTINEL";
1229        const SOZU_ID_HEADER_SECRET: &str = "HTTPS_LISTENER_SOZU_ID_HEADER_SECRET_SENTINEL";
1230        const ANSWER_KEY_SECRET: &str = "HTTPS_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1231        const ANSWER_VALUE_SECRET: &str = "HTTPS_LISTENER_ANSWER_VALUE_SECRET_SENTINEL";
1232
1233        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1234        let address: SocketAddr = "127.0.0.1:8443"
1235            .parse()
1236            .expect("test HTTPS listener address must parse");
1237        let public_address: SocketAddr = "203.0.113.1:443"
1238            .parse()
1239            .expect("test public HTTPS listener address must parse");
1240        let listener = HttpsListenerConfig {
1241            address: address.into(),
1242            public_address: Some(public_address.into()),
1243            sticky_name: long_value(STICKY_NAME_SECRET),
1244            versions: vec![TlsVersion::TlsV13 as i32],
1245            cipher_list: vec![long_value(CIPHER_LIST_SECRET)],
1246            cipher_suites: vec![long_value(CIPHER_SUITES_SECRET)],
1247            signature_algorithms: vec![long_value(SIGNATURE_ALGORITHMS_SECRET)],
1248            groups_list: vec![long_value(GROUPS_LIST_SECRET)],
1249            certificate: Some(long_value(CERTIFICATE_SECRET)),
1250            certificate_chain: vec![long_value(CHAIN_SECRET)],
1251            key: Some(long_value(KEY_SECRET)),
1252            http_answers: Some(CustomHttpAnswers {
1253                answer_404: Some(long_value(HTTP_ANSWER_SECRET)),
1254                ..Default::default()
1255            }),
1256            alpn_protocols: vec![long_value(ALPN_SECRET)],
1257            sozu_id_header: Some(long_value(SOZU_ID_HEADER_SECRET)),
1258            answers: BTreeMap::from([(
1259                long_value(ANSWER_KEY_SECRET),
1260                long_value(ANSWER_VALUE_SECRET),
1261            )]),
1262            ..Default::default()
1263        };
1264        let add_https_listener = RequestType::AddHttpsListener(listener.clone());
1265        let request = Request::from(add_https_listener.clone());
1266        let worker_request = WorkerRequest {
1267            id: "safe-add-https-listener-id".to_owned(),
1268            content: request.clone(),
1269        };
1270        let state = ConfigState {
1271            https_listeners: BTreeMap::from([(address.into(), listener.clone())]),
1272            ..Default::default()
1273        };
1274        let debug_outputs = [
1275            ("HttpsListenerConfig", format!("{listener:?}")),
1276            (
1277                "RequestType::AddHttpsListener",
1278                format!("{add_https_listener:?}"),
1279            ),
1280            ("Request", format!("{request:?}")),
1281            ("WorkerRequest", format!("{worker_request:?}")),
1282            ("ConfigState", format!("{state:?}")),
1283        ];
1284        let secrets = [
1285            STICKY_NAME_SECRET,
1286            CIPHER_LIST_SECRET,
1287            CIPHER_SUITES_SECRET,
1288            SIGNATURE_ALGORITHMS_SECRET,
1289            GROUPS_LIST_SECRET,
1290            CERTIFICATE_SECRET,
1291            CHAIN_SECRET,
1292            KEY_SECRET,
1293            HTTP_ANSWER_SECRET,
1294            ALPN_SECRET,
1295            SOZU_ID_HEADER_SECRET,
1296            ANSWER_KEY_SECRET,
1297            ANSWER_VALUE_SECRET,
1298        ];
1299        let mut leaks = Vec::new();
1300        for (label, output) in &debug_outputs {
1301            for secret in secrets {
1302                if output.contains(secret) {
1303                    leaks.push(format!("{label}:{secret}"));
1304                }
1305            }
1306        }
1307        assert!(
1308            leaks.is_empty(),
1309            "HTTPS listener Debug leaked secret markers through {}",
1310            leaks.join(", ")
1311        );
1312
1313        let direct_output = &debug_outputs[0].1;
1314        let expected_metadata = [
1315            format!("sticky_name_len: {}", long_value(STICKY_NAME_SECRET).len()),
1316            "versions_count: 1".to_owned(),
1317            "cipher_list_count: 1".to_owned(),
1318            format!("cipher_list_len: {}", long_value(CIPHER_LIST_SECRET).len()),
1319            "cipher_suites_count: 1".to_owned(),
1320            "signature_algorithms_count: 1".to_owned(),
1321            "groups_list_count: 1".to_owned(),
1322            "certificate: Some(\"[redacted]\")".to_owned(),
1323            format!(
1324                "certificate_len: Some({})",
1325                long_value(CERTIFICATE_SECRET).len()
1326            ),
1327            "certificate_chain: \"[redacted]\"".to_owned(),
1328            "certificate_chain_count: 1".to_owned(),
1329            "key: Some(\"[redacted]\")".to_owned(),
1330            "http_answers_count: Some(1)".to_owned(),
1331            "alpn_protocols_count: 1".to_owned(),
1332            format!(
1333                "sozu_id_header_len: Some({})",
1334                long_value(SOZU_ID_HEADER_SECRET).len()
1335            ),
1336            "answers_count: 1".to_owned(),
1337        ];
1338        for safe_metadata in expected_metadata {
1339            assert!(
1340                direct_output.contains(&safe_metadata),
1341                "HttpsListenerConfig Debug omitted safe metadata {safe_metadata}: {direct_output}"
1342            );
1343        }
1344        for (label, output) in debug_outputs {
1345            assert!(
1346                output.len() <= 4096,
1347                "{label} Debug output is not bounded: {} bytes",
1348                output.len()
1349            );
1350        }
1351    }
1352
1353    #[test]
1354    fn header_debug_redacts_key_and_value() {
1355        const KEY_SECRET: &str = "HEADER_KEY_SECRET_SENTINEL";
1356        const VALUE_SECRET: &str = "HEADER_VALUE_SECRET_SENTINEL";
1357
1358        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1359        let header = Header {
1360            position: HeaderPosition::Unspecified as i32,
1361            key: long_value(KEY_SECRET),
1362            val: long_value(VALUE_SECRET),
1363        };
1364        let output = format!("{header:?}");
1365
1366        for secret in [KEY_SECRET, VALUE_SECRET] {
1367            assert!(
1368                !output.contains(secret),
1369                "Header Debug leaked user-controlled marker {secret}"
1370            );
1371        }
1372        for metadata in [
1373            "position: 0".to_owned(),
1374            format!("key_len: {}", long_value(KEY_SECRET).len()),
1375            format!("val_len: {}", long_value(VALUE_SECRET).len()),
1376        ] {
1377            assert!(
1378                output.contains(&metadata),
1379                "Header Debug omitted safe metadata {metadata}: {output}"
1380            );
1381        }
1382        assert!(
1383            output.len() <= 256,
1384            "Header Debug output is not bounded: {} bytes",
1385            output.len()
1386        );
1387    }
1388
1389    #[test]
1390    fn http_frontend_request_debug_redacts_user_controlled_material() {
1391        const CLUSTER_SECRET: &str = "HTTP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1392        const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
1393        const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
1394        const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
1395        const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1396        const TAG_VALUE_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1397        const REDIRECT_SECRET: &str = "HTTP_FRONTEND_REDIRECT_SECRET_SENTINEL";
1398        const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
1399        const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
1400        const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
1401        const HEADER_VALUE_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
1402        const PREDICATE_NAME_SECRET: &str = "HTTP_FRONTEND_PREDICATE_NAME_SECRET_SENTINEL";
1403        const PREDICATE_VALUE_SECRET: &str = "HTTP_FRONTEND_PREDICATE_VALUE_SECRET_SENTINEL";
1404
1405        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1406        let frontend = RequestHttpFrontend {
1407            cluster_id: Some(long_value(CLUSTER_SECRET)),
1408            address: Default::default(),
1409            hostname: long_value(HOSTNAME_SECRET),
1410            path: PathRule {
1411                value: long_value(PATH_SECRET),
1412                kind: PathRuleKind::Prefix as i32,
1413            },
1414            method: Some(long_value(METHOD_SECRET)),
1415            position: RulePosition::Tree as i32,
1416            tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1417            redirect: Some(RedirectPolicy::Forward as i32),
1418            required_auth: Some(true),
1419            redirect_scheme: Some(RedirectScheme::UseSame as i32),
1420            redirect_template: Some(long_value(REDIRECT_SECRET)),
1421            rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
1422            rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
1423            rewrite_port: Some(8443),
1424            headers: vec![Header {
1425                position: HeaderPosition::Request as i32,
1426                key: long_value(HEADER_KEY_SECRET),
1427                val: long_value(HEADER_VALUE_SECRET),
1428            }],
1429            hsts: None,
1430            header_predicates: vec![HeaderPredicate {
1431                name: long_value(PREDICATE_NAME_SECRET),
1432                kind: HeaderMatchKind::Exact as i32,
1433                value: Some(long_value(PREDICATE_VALUE_SECRET)),
1434            }],
1435        };
1436        let request_type = RequestType::AddHttpsFrontend(frontend.clone());
1437        let request = Request::from(request_type.clone());
1438        let worker_request = WorkerRequest {
1439            id: "safe-http-frontend-request-id".to_owned(),
1440            content: request.clone(),
1441        };
1442        let debug_outputs = [
1443            ("RequestHttpFrontend", format!("{frontend:?}")),
1444            ("RequestType", format!("{request_type:?}")),
1445            ("Request", format!("{request:?}")),
1446            ("WorkerRequest", format!("{worker_request:?}")),
1447        ];
1448        let secrets = [
1449            CLUSTER_SECRET,
1450            HOSTNAME_SECRET,
1451            PATH_SECRET,
1452            METHOD_SECRET,
1453            TAG_KEY_SECRET,
1454            TAG_VALUE_SECRET,
1455            REDIRECT_SECRET,
1456            REWRITE_HOST_SECRET,
1457            REWRITE_PATH_SECRET,
1458            HEADER_KEY_SECRET,
1459            HEADER_VALUE_SECRET,
1460            PREDICATE_NAME_SECRET,
1461            PREDICATE_VALUE_SECRET,
1462        ];
1463
1464        for (label, output) in &debug_outputs {
1465            for secret in secrets {
1466                assert!(
1467                    !output.contains(secret),
1468                    "{label} Debug leaked user-controlled marker {secret}: {output}"
1469                );
1470            }
1471            assert!(
1472                output.len() <= 2048,
1473                "{label} Debug output is not bounded: {} bytes",
1474                output.len()
1475            );
1476        }
1477
1478        let direct_output = &debug_outputs[0].1;
1479        let expected_metadata = [
1480            format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
1481            format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
1482            format!("path_len: {}", long_value(PATH_SECRET).len()),
1483            format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
1484            "tags_count: 1".to_owned(),
1485            "headers_count: 1".to_owned(),
1486            "header_predicates_count: 1".to_owned(),
1487        ];
1488        for safe_metadata in expected_metadata {
1489            assert!(
1490                direct_output.contains(&safe_metadata),
1491                "RequestHttpFrontend Debug omitted safe metadata {safe_metadata}: {direct_output}"
1492            );
1493        }
1494    }
1495
1496    #[test]
1497    fn certificate_query_request_debug_redacts_filters_across_command_wrappers() {
1498        const DOMAIN_SECRET: &str = "QUERY_REQUEST_DOMAIN_SECRET_SENTINEL";
1499        const FINGERPRINT_SECRET: &str = "QUERY_REQUEST_FINGERPRINT_SECRET_SENTINEL";
1500        const REQUEST_ID_SECRET: &str = "QUERY_REQUEST_ID_SECRET_SENTINEL";
1501
1502        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1503        let filters = QueryCertificatesFilters {
1504            domain: Some(long_value(DOMAIN_SECRET)),
1505            fingerprint: Some(long_value(FINGERPRINT_SECRET)),
1506        };
1507        let request_type = RequestType::QueryCertificatesFromWorkers(filters.clone());
1508        let request = Request::from(request_type.clone());
1509        let worker_request = WorkerRequest {
1510            id: long_value(REQUEST_ID_SECRET),
1511            content: request.clone(),
1512        };
1513        let outputs = [
1514            ("QueryCertificatesFilters", format!("{filters:?}")),
1515            ("RequestType", format!("{request_type:?}")),
1516            ("Request", format!("{request:?}")),
1517            ("WorkerRequest", format!("{worker_request:?}")),
1518        ];
1519
1520        for (label, output) in &outputs {
1521            for secret in [DOMAIN_SECRET, FINGERPRINT_SECRET, REQUEST_ID_SECRET] {
1522                assert!(
1523                    !output.contains(secret),
1524                    "{label} Debug leaked certificate-query marker {secret}: {output}"
1525                );
1526            }
1527            assert!(
1528                output.len() <= 1024,
1529                "{label} Debug output is not bounded: {} bytes",
1530                output.len()
1531            );
1532        }
1533
1534        let filter_output = &outputs[0].1;
1535        for metadata in [
1536            format!("domain_len: Some({})", long_value(DOMAIN_SECRET).len()),
1537            format!(
1538                "fingerprint_len: Some({})",
1539                long_value(FINGERPRINT_SECRET).len()
1540            ),
1541        ] {
1542            assert!(
1543                filter_output.contains(&metadata),
1544                "QueryCertificatesFilters Debug omitted bounded metadata {metadata}: {filter_output}"
1545            );
1546        }
1547        assert!(
1548            outputs[3]
1549                .1
1550                .contains(&format!("id_len: {}", long_value(REQUEST_ID_SECRET).len())),
1551            "WorkerRequest Debug omitted the bounded request-id length: {}",
1552            outputs[3].1
1553        );
1554    }
1555
1556    #[test]
1557    fn silent_close_frontend_request_debug_keeps_redacted_payload() {
1558        const HOSTNAME_SECRET: &str = "SILENT_CLOSE_HOSTNAME_SECRET_SENTINEL";
1559
1560        let frontend = RequestHttpFrontend {
1561            hostname: HOSTNAME_SECRET.to_owned(),
1562            path: PathRule {
1563                value: "/".to_owned(),
1564                kind: PathRuleKind::Prefix as i32,
1565            },
1566            position: RulePosition::Tree as i32,
1567            redirect: Some(RedirectPolicy::SilentClose as i32),
1568            ..Default::default()
1569        };
1570
1571        for (name, request_type) in [
1572            (
1573                "AddHttpFrontendWithSilentClose",
1574                RequestType::AddHttpFrontendWithSilentClose(frontend.clone()),
1575            ),
1576            (
1577                "AddHttpsFrontendWithSilentClose",
1578                RequestType::AddHttpsFrontendWithSilentClose(frontend.clone()),
1579            ),
1580        ] {
1581            let output = format!("{request_type:?}");
1582            assert!(
1583                output.contains(&format!("hostname_len: {}", HOSTNAME_SECRET.len())),
1584                "{name} Debug dropped the frontend payload: {output}"
1585            );
1586            assert!(
1587                !output.contains(HOSTNAME_SECRET),
1588                "{name} Debug leaked the hostname: {output}"
1589            );
1590        }
1591    }
1592
1593    #[test]
1594    fn certificate_query_response_debug_redacts_results_across_worker_wrappers() {
1595        const DOMAIN_SECRET: &str = "QUERY_RESPONSE_DOMAIN_SECRET_SENTINEL";
1596        const FINGERPRINT_SECRET: &str = "QUERY_RESPONSE_FINGERPRINT_SECRET_SENTINEL";
1597        const MAP_KEY_SECRET: &str = "QUERY_RESPONSE_MAP_KEY_SECRET_SENTINEL";
1598        const CERTIFICATE_SECRET: &str = "QUERY_RESPONSE_CERTIFICATE_SECRET_SENTINEL";
1599        const KEY_SECRET: &str = "QUERY_RESPONSE_KEY_SECRET_SENTINEL";
1600        const NAME_SECRET: &str = "QUERY_RESPONSE_NAME_SECRET_SENTINEL";
1601        const WORKER_ID_SECRET: &str = "QUERY_RESPONSE_WORKER_ID_SECRET_SENTINEL";
1602        const RESPONSE_ID_SECRET: &str = "QUERY_RESPONSE_ID_SECRET_SENTINEL";
1603        const MESSAGE_SECRET: &str = "QUERY_RESPONSE_MESSAGE_SECRET_SENTINEL";
1604
1605        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1606        let summary = CertificateSummary {
1607            domain: long_value(DOMAIN_SECRET),
1608            fingerprint: long_value(FINGERPRINT_SECRET),
1609        };
1610        let by_address = CertificatesByAddress {
1611            address: Default::default(),
1612            certificate_summaries: vec![summary.clone()],
1613        };
1614        let list = ListOfCertificatesByAddress {
1615            certificates: vec![by_address.clone()],
1616        };
1617        let worker_content =
1618            ResponseContent::from(ContentType::CertificatesByAddress(list.clone()));
1619        let worker_response = WorkerResponse {
1620            id: long_value(RESPONSE_ID_SECRET),
1621            status: ResponseStatus::Ok as i32,
1622            message: long_value(MESSAGE_SECRET),
1623            content: Some(worker_content.clone()),
1624        };
1625        let worker_responses = WorkerResponses {
1626            map: BTreeMap::from([(long_value(WORKER_ID_SECRET), worker_content.clone())]),
1627        };
1628        let gathered_content =
1629            ResponseContent::from(ContentType::WorkerResponses(worker_responses.clone()));
1630        let main_certificates = CertificatesWithFingerprints {
1631            certs: BTreeMap::from([(
1632                long_value(MAP_KEY_SECRET),
1633                CertificateAndKey {
1634                    certificate: long_value(CERTIFICATE_SECRET),
1635                    certificate_chain: Vec::new(),
1636                    key: long_value(KEY_SECRET),
1637                    versions: Vec::new(),
1638                    names: vec![long_value(NAME_SECRET)],
1639                },
1640            )]),
1641        };
1642        let main_content = ResponseContent::from(ContentType::CertificatesWithFingerprints(
1643            main_certificates.clone(),
1644        ));
1645        let outputs = [
1646            ("CertificateSummary", format!("{summary:?}")),
1647            ("CertificatesByAddress", format!("{by_address:?}")),
1648            ("ListOfCertificatesByAddress", format!("{list:?}")),
1649            ("worker ResponseContent", format!("{worker_content:?}")),
1650            ("WorkerResponse", format!("{worker_response:?}")),
1651            ("WorkerResponses", format!("{worker_responses:?}")),
1652            ("gathered ResponseContent", format!("{gathered_content:?}")),
1653            (
1654                "CertificatesWithFingerprints",
1655                format!("{main_certificates:?}"),
1656            ),
1657            ("main ResponseContent", format!("{main_content:?}")),
1658        ];
1659        let secrets = [
1660            DOMAIN_SECRET,
1661            FINGERPRINT_SECRET,
1662            MAP_KEY_SECRET,
1663            CERTIFICATE_SECRET,
1664            KEY_SECRET,
1665            NAME_SECRET,
1666            WORKER_ID_SECRET,
1667            RESPONSE_ID_SECRET,
1668            MESSAGE_SECRET,
1669        ];
1670
1671        for (label, output) in &outputs {
1672            for secret in secrets {
1673                assert!(
1674                    !output.contains(secret),
1675                    "{label} Debug leaked certificate-query marker {secret}: {output}"
1676                );
1677            }
1678            assert!(
1679                output.len() <= 1024,
1680                "{label} Debug output is not bounded: {} bytes",
1681                output.len()
1682            );
1683        }
1684
1685        for metadata in ["listeners_count: 1", "certificates_count: 1"] {
1686            assert!(
1687                outputs[2].1.contains(metadata),
1688                "ListOfCertificatesByAddress Debug omitted bounded metadata {metadata}: {}",
1689                outputs[2].1
1690            );
1691        }
1692        for metadata in ["id_len:", "message_len:", "content_type:"] {
1693            assert!(
1694                outputs[4].1.contains(metadata),
1695                "WorkerResponse Debug omitted bounded metadata {metadata}: {}",
1696                outputs[4].1
1697            );
1698        }
1699    }
1700
1701    #[test]
1702    fn certificate_debug_redacts_pem_material() {
1703        const CERTIFICATE_SECRET: &str = "CERTIFICATE_SECRET_SENTINEL";
1704        const CHAIN_SECRET: &str = "CHAIN_SECRET_SENTINEL";
1705        const KEY_SECRET: &str = "KEY_SECRET_SENTINEL";
1706        const NAME_SECRET: &str = "CERTIFICATE_NAME_SECRET_SENTINEL";
1707
1708        let certificate = CertificateAndKey {
1709            certificate: CERTIFICATE_SECRET.to_owned(),
1710            certificate_chain: vec![CHAIN_SECRET.to_owned()],
1711            key: KEY_SECRET.to_owned(),
1712            versions: vec![i32::MIN; 1_024],
1713            names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1714        };
1715        let add_certificate = AddCertificate {
1716            address: Default::default(),
1717            certificate: certificate.clone(),
1718            expired_at: None,
1719        };
1720        let replace_certificate = ReplaceCertificate {
1721            address: Default::default(),
1722            new_certificate: certificate.clone(),
1723            old_fingerprint: "safe-old-fingerprint".to_owned(),
1724            new_expired_at: None,
1725        };
1726        let add_request = Request::from(RequestType::AddCertificate(add_certificate.clone()));
1727        let replace_request =
1728            Request::from(RequestType::ReplaceCertificate(replace_certificate.clone()));
1729        let add_worker_request = WorkerRequest {
1730            id: "safe-add-request-id".to_owned(),
1731            content: add_request.clone(),
1732        };
1733        let replace_worker_request = WorkerRequest {
1734            id: "safe-replace-request-id".to_owned(),
1735            content: replace_request.clone(),
1736        };
1737        let state_address: SocketAddr = "127.0.0.1:443"
1738            .parse()
1739            .expect("test certificate address must parse");
1740        let state = ConfigState {
1741            certificates: HashMap::from([(
1742                state_address,
1743                HashMap::from([(Fingerprint(vec![0; 32]), certificate.clone())]),
1744            )]),
1745            ..Default::default()
1746        };
1747
1748        let debug_outputs = [
1749            format!("{certificate:?}"),
1750            format!("{add_certificate:?}"),
1751            format!("{replace_certificate:?}"),
1752            format!("{add_request:?}"),
1753            format!("{replace_request:?}"),
1754            format!("{add_worker_request:?}"),
1755            format!("{replace_worker_request:?}"),
1756        ];
1757        let state_debug = format!("{state:?}");
1758
1759        for output in debug_outputs {
1760            for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1761                assert!(
1762                    !output.contains(secret),
1763                    "debug output leaked secret marker {secret}: {output}"
1764                );
1765            }
1766            assert!(
1767                output.contains("[redacted]"),
1768                "debug output must make redaction explicit: {output}"
1769            );
1770            assert!(
1771                output.contains("names_count: 1") && output.contains("names_len: 4128"),
1772                "debug output must retain bounded certificate-name metadata: {output}"
1773            );
1774            assert!(
1775                output.contains("versions_count: 1024"),
1776                "debug output must retain bounded TLS-version metadata: {output}"
1777            );
1778            assert!(
1779                output.len() <= 4096,
1780                "certificate Debug output is not bounded: {} bytes",
1781                output.len()
1782            );
1783        }
1784
1785        for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1786            assert!(
1787                !state_debug.contains(secret),
1788                "ConfigState Debug leaked secret marker {secret}: {state_debug}"
1789            );
1790        }
1791        assert!(
1792            state_debug.contains("certificate_addresses_count: 1")
1793                && state_debug.contains("certificates_count: 1"),
1794            "ConfigState Debug must retain count-only certificate metadata: {state_debug}"
1795        );
1796        assert!(
1797            state_debug.len() <= 4096,
1798            "ConfigState Debug output is not bounded: {} bytes",
1799            state_debug.len()
1800        );
1801    }
1802
1803    #[test]
1804    fn certificate_fingerprint_control_debug_is_redacted_across_wrappers() {
1805        const FINGERPRINT_SECRET: &str = "CERTIFICATE_CONTROL_FINGERPRINT_SECRET_SENTINEL";
1806
1807        let fingerprint = format!("{FINGERPRINT_SECRET}{}", "f".repeat(4096));
1808        let remove = RemoveCertificate {
1809            address: Default::default(),
1810            fingerprint: fingerprint.clone(),
1811        };
1812        let replace = ReplaceCertificate {
1813            address: Default::default(),
1814            new_certificate: CertificateAndKey::default(),
1815            old_fingerprint: fingerprint.clone(),
1816            new_expired_at: None,
1817        };
1818        let remove_request = Request::from(RequestType::RemoveCertificate(remove.clone()));
1819        let replace_request = Request::from(RequestType::ReplaceCertificate(replace.clone()));
1820        let outputs = [
1821            ("RemoveCertificate", format!("{remove:?}")),
1822            ("ReplaceCertificate", format!("{replace:?}")),
1823            ("remove Request", format!("{remove_request:?}")),
1824            ("replace Request", format!("{replace_request:?}")),
1825            (
1826                "remove WorkerRequest",
1827                format!(
1828                    "{:?}",
1829                    WorkerRequest {
1830                        id: "remove-certificate-test".to_owned(),
1831                        content: remove_request,
1832                    }
1833                ),
1834            ),
1835            (
1836                "replace WorkerRequest",
1837                format!(
1838                    "{:?}",
1839                    WorkerRequest {
1840                        id: "replace-certificate-test".to_owned(),
1841                        content: replace_request,
1842                    }
1843                ),
1844            ),
1845        ];
1846
1847        for (label, output) in outputs {
1848            assert!(
1849                !output.contains(FINGERPRINT_SECRET),
1850                "{label} Debug leaked the certificate fingerprint: {output}"
1851            );
1852            assert!(
1853                output.contains(&format!("fingerprint_len: {}", fingerprint.len())),
1854                "{label} Debug omitted bounded fingerprint metadata: {output}"
1855            );
1856            assert!(
1857                output.len() <= 1024,
1858                "{label} Debug output is not bounded: {} bytes",
1859                output.len()
1860            );
1861        }
1862    }
1863
1864    #[test]
1865    fn cluster_debug_redacts_answers_and_authorization_data_across_wrappers() {
1866        const CLUSTER_SECRET: &str = "CLUSTER_ID_SECRET_SENTINEL";
1867        const LEGACY_ANSWER_SECRET: &str = "CLUSTER_LEGACY_ANSWER_SECRET_SENTINEL";
1868        const ANSWER_KEY_SECRET: &str = "CLUSTER_ANSWER_KEY_SECRET_SENTINEL";
1869        const ANSWER_BODY_SECRET: &str = "CLUSTER_ANSWER_BODY_SECRET_SENTINEL";
1870        const AUTH_HASH_SECRET: &str = "CLUSTER_AUTH_HASH_SECRET_SENTINEL";
1871        const REALM_SECRET: &str = "CLUSTER_AUTH_REALM_SECRET_SENTINEL";
1872
1873        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1874        let cluster = Cluster {
1875            cluster_id: long_value(CLUSTER_SECRET),
1876            answer_503: Some(long_value(LEGACY_ANSWER_SECRET)),
1877            answers: BTreeMap::from([(
1878                long_value(ANSWER_KEY_SECRET),
1879                long_value(ANSWER_BODY_SECRET),
1880            )]),
1881            authorized_hashes: vec![long_value(AUTH_HASH_SECRET)],
1882            www_authenticate: Some(long_value(REALM_SECRET)),
1883            ..Default::default()
1884        };
1885        let request = Request::from(RequestType::AddCluster(cluster.clone()));
1886        let outputs = [
1887            ("Cluster", format!("{cluster:?}")),
1888            ("Request", format!("{request:?}")),
1889            (
1890                "WorkerRequest",
1891                format!(
1892                    "{:?}",
1893                    WorkerRequest {
1894                        id: "cluster-redaction-test".to_owned(),
1895                        content: request,
1896                    }
1897                ),
1898            ),
1899        ];
1900
1901        for (label, output) in outputs {
1902            for secret in [
1903                CLUSTER_SECRET,
1904                LEGACY_ANSWER_SECRET,
1905                ANSWER_KEY_SECRET,
1906                ANSWER_BODY_SECRET,
1907                AUTH_HASH_SECRET,
1908                REALM_SECRET,
1909            ] {
1910                assert!(
1911                    !output.contains(secret),
1912                    "{label} Debug leaked cluster marker {secret}: {output}"
1913                );
1914            }
1915            for metadata in [
1916                "cluster_id_len:",
1917                "answers_count: 1",
1918                "authorized_hashes_count: 1",
1919            ] {
1920                assert!(
1921                    output.contains(metadata),
1922                    "{label} Debug omitted bounded cluster metadata {metadata}: {output}"
1923                );
1924            }
1925            assert!(
1926                output.len() <= 2048,
1927                "{label} Debug output is not bounded: {} bytes",
1928                output.len()
1929            );
1930        }
1931    }
1932
1933    #[test]
1934    fn tcp_frontend_debug_redacts_stored_sni_routing_data_across_wrappers() {
1935        const CLUSTER_SECRET: &str = "TCP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1936        const TAG_KEY_SECRET: &str = "TCP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1937        const TAG_VALUE_SECRET: &str = "TCP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1938        const SNI_SECRET: &str = "TCP_FRONTEND_SNI_SECRET_SENTINEL";
1939        const ALPN_SECRET: &str = "TCP_FRONTEND_ALPN_SECRET_SENTINEL";
1940
1941        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1942        let frontend = RequestTcpFrontend {
1943            cluster_id: long_value(CLUSTER_SECRET),
1944            address: Default::default(),
1945            tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1946            sni: Some(long_value(SNI_SECRET)),
1947            alpn: vec![long_value(ALPN_SECRET)],
1948        };
1949        let request = Request::from(RequestType::AddTcpFrontend(frontend.clone()));
1950        let outputs = [
1951            ("RequestTcpFrontend", format!("{frontend:?}")),
1952            ("Request", format!("{request:?}")),
1953            (
1954                "WorkerRequest",
1955                format!(
1956                    "{:?}",
1957                    WorkerRequest {
1958                        id: "tcp-frontend-redaction-test".to_owned(),
1959                        content: request,
1960                    }
1961                ),
1962            ),
1963        ];
1964
1965        for (label, output) in outputs {
1966            for secret in [
1967                CLUSTER_SECRET,
1968                TAG_KEY_SECRET,
1969                TAG_VALUE_SECRET,
1970                SNI_SECRET,
1971                ALPN_SECRET,
1972            ] {
1973                assert!(
1974                    !output.contains(secret),
1975                    "{label} Debug leaked TCP frontend marker {secret}: {output}"
1976                );
1977            }
1978            for metadata in [
1979                "cluster_id_len:",
1980                "tags_count: 1",
1981                "sni_len: Some(",
1982                "alpn_count: 1",
1983            ] {
1984                assert!(
1985                    output.contains(metadata),
1986                    "{label} Debug omitted bounded TCP frontend metadata {metadata}: {output}"
1987                );
1988            }
1989            assert!(
1990                output.len() <= 1024,
1991                "{label} Debug output is not bounded: {} bytes",
1992                output.len()
1993            );
1994        }
1995    }
1996
1997    #[test]
1998    fn merge_relocates_single_worker_to_top_level() {
1999        // Regression: a one-worker fleet must populate `clusters` and
2000        // `proxying` so CLI/TUI consumers reading those maps see the
2001        // worker's data. `std::mem::take(&mut self.workers)` empties the
2002        // per-worker map after relocation, which is the documented
2003        // contract when the caller asked for the merged shape.
2004        let mut worker = WorkerMetrics {
2005            proxy: BTreeMap::new(),
2006            clusters: BTreeMap::new(),
2007        };
2008        worker.proxy.insert(
2009            "requests".to_owned(),
2010            FilteredMetrics {
2011                inner: Some(Inner::Count(42)),
2012            },
2013        );
2014        let mut cluster = ClusterMetrics {
2015            cluster: BTreeMap::new(),
2016            backends: Vec::new(),
2017        };
2018        cluster.cluster.insert(
2019            "requests".to_owned(),
2020            FilteredMetrics {
2021                inner: Some(Inner::Count(7)),
2022            },
2023        );
2024        worker.clusters.insert("cluster-a".to_owned(), cluster);
2025
2026        let mut agg = AggregatedMetrics {
2027            main: BTreeMap::new(),
2028            workers: BTreeMap::from([("0".to_owned(), worker)]),
2029            clusters: BTreeMap::new(),
2030            proxying: BTreeMap::new(),
2031        };
2032
2033        agg.merge_metrics();
2034
2035        assert!(
2036            agg.workers.is_empty(),
2037            "merge takes ownership of the per-worker map"
2038        );
2039        assert_eq!(
2040            agg.proxying.get("requests"),
2041            Some(&FilteredMetrics {
2042                inner: Some(Inner::Count(42)),
2043            }),
2044            "single worker's proxy counter must surface in proxying"
2045        );
2046        let cluster_a = agg
2047            .clusters
2048            .get("cluster-a")
2049            .expect("cluster row must surface in top-level clusters");
2050        assert_eq!(
2051            cluster_a.cluster.get("requests"),
2052            Some(&FilteredMetrics {
2053                inner: Some(Inner::Count(7)),
2054            })
2055        );
2056    }
2057
2058    #[test]
2059    fn merge_counts_and_gauges() {
2060        let mut gauge_a = FilteredMetrics {
2061            inner: Some(Inner::Gauge(4)),
2062        };
2063        let gauge_b = FilteredMetrics {
2064            inner: Some(Inner::Gauge(4)),
2065        };
2066
2067        gauge_a.merge(&gauge_b);
2068
2069        assert_eq!(
2070            gauge_a,
2071            FilteredMetrics {
2072                inner: Some(Inner::Gauge(8)),
2073            }
2074        );
2075
2076        let mut count_a = FilteredMetrics {
2077            inner: Some(Inner::Count(3)),
2078        };
2079        let count_b = FilteredMetrics {
2080            inner: Some(Inner::Count(3)),
2081        };
2082
2083        count_a.merge(&count_b);
2084
2085        assert_eq!(
2086            count_a,
2087            FilteredMetrics {
2088                inner: Some(Inner::Count(6)),
2089            }
2090        );
2091    }
2092
2093    #[test]
2094    fn merge_percentiles_takes_max_per_quantile() {
2095        // Multi-worker percentile aggregation propagates the worst-case
2096        // quantile across workers and accumulates samples + sum so the
2097        // surfaced summary remains the "is anyone slow?" upper bound.
2098        let mut left = FilteredMetrics {
2099            inner: Some(Inner::Percentiles(Percentiles {
2100                samples: 100,
2101                p_50: 5,
2102                p_90: 20,
2103                p_99: 100,
2104                p_99_9: 200,
2105                p_99_99: 250,
2106                p_99_999: 300,
2107                p_100: 400,
2108                sum: 12_000,
2109            })),
2110        };
2111        let right = FilteredMetrics {
2112            inner: Some(Inner::Percentiles(Percentiles {
2113                samples: 50,
2114                p_50: 7,
2115                p_90: 15,
2116                p_99: 80,
2117                p_99_9: 240,
2118                p_99_99: 245,
2119                p_99_999: 290,
2120                p_100: 380,
2121                sum: 6_000,
2122            })),
2123        };
2124        left.merge(&right);
2125        assert_eq!(
2126            left,
2127            FilteredMetrics {
2128                inner: Some(Inner::Percentiles(Percentiles {
2129                    samples: 150,
2130                    p_50: 7,
2131                    p_90: 20,
2132                    p_99: 100,
2133                    p_99_9: 240,
2134                    p_99_99: 250,
2135                    p_99_999: 300,
2136                    p_100: 400,
2137                    sum: 18_000,
2138                })),
2139            }
2140        );
2141    }
2142
2143    #[test]
2144    fn merge_histograms() {
2145        let mut histogram_a = FilteredMetrics {
2146            inner: Some(Inner::Histogram(FilteredHistogram {
2147                sum: 95,
2148                count: 30,
2149                buckets: vec![
2150                    Bucket { le: 0, count: 1 },
2151                    Bucket { le: 1, count: 2 },
2152                    Bucket { le: 3, count: 10 },
2153                    Bucket { le: 7, count: 25 },
2154                    Bucket { le: 15, count: 27 },
2155                    Bucket { le: 31, count: 30 },
2156                ],
2157            })),
2158        };
2159
2160        let histogram_b = FilteredMetrics {
2161            inner: Some(Inner::Histogram(FilteredHistogram {
2162                sum: 82,
2163                count: 40,
2164                buckets: vec![
2165                    Bucket { le: 0, count: 0 },
2166                    Bucket { le: 1, count: 0 },
2167                    Bucket { le: 3, count: 12 },
2168                    Bucket { le: 7, count: 30 },
2169                    Bucket { le: 15, count: 40 },
2170                    // note: there is no bucket for "le: 31"
2171                ],
2172            })),
2173        };
2174
2175        histogram_a.merge(&histogram_b);
2176
2177        let merged_histogram = FilteredMetrics {
2178            inner: Some(Inner::Histogram(FilteredHistogram {
2179                sum: 177,
2180                count: 70,
2181                buckets: vec![
2182                    Bucket { le: 0, count: 1 },
2183                    Bucket { le: 1, count: 2 },
2184                    Bucket { le: 3, count: 22 },
2185                    Bucket { le: 7, count: 55 },
2186                    Bucket { le: 15, count: 67 },
2187                    Bucket { le: 31, count: 70 }, // note: the total count of histogram b is added, even though histogram b has no bucket
2188                ],
2189            })),
2190        };
2191
2192        assert_eq!(histogram_a, merged_histogram);
2193    }
2194
2195    const HEALTH_CHECK_SECRET: &str = "SYNTHETIC-DEBUG-HC-SECRET-a91d";
2196    const HEALTH_CHECK_FINGERPRINT: &str = "sozu-hc-hmac-sha256:v1:00112233445566778899aabbccddeeff:0000000000000000000000000000000000000000000000000000000000000000";
2197
2198    fn fingerprinted_health_check() -> super::command::HealthCheckConfig {
2199        use super::command::{HealthCheckConfig, HealthCheckHeader};
2200        HealthCheckConfig {
2201            uri: "/".to_owned(),
2202            interval: 10,
2203            timeout: 5,
2204            healthy_threshold: 3,
2205            unhealthy_threshold: 3,
2206            headers: vec![HealthCheckHeader {
2207                name: "Authorization".to_owned(),
2208                fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2209            }],
2210            ..Default::default()
2211        }
2212    }
2213
2214    /// `Debug` of a header prints a bounded name and its fingerprint, and the
2215    /// ingress carrier prints a placeholder instead of the bytes or their
2216    /// length, alone or inside a request.
2217    #[test]
2218    fn health_check_header_debug_shows_metadata_and_never_the_ingress_value() {
2219        use super::command::{
2220            HealthCheckHeader, HealthCheckHeaderValue, SetHealthCheck, SetProtectedHealthCheck,
2221        };
2222
2223        let long_name = format!("X-{}", "n".repeat(200));
2224        let alone = format!(
2225            "{:?}",
2226            HealthCheckHeader {
2227                name: long_name.clone(),
2228                fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2229            }
2230        );
2231        assert!(!alone.contains(&long_name), "the name is bounded: {alone}");
2232        assert!(alone.contains("name_truncated: true"), "{alone}");
2233        assert!(alone.contains(HEALTH_CHECK_FINGERPRINT), "{alone}");
2234        // A multi-byte name is cut on a char boundary, never mid-character.
2235        let wide = format!(
2236            "{:?}",
2237            HealthCheckHeader {
2238                name: "\u{e9}".repeat(100),
2239                fingerprint: String::new(),
2240            }
2241        );
2242        assert!(wide.contains("name_truncated: true"), "{wide}");
2243
2244        let config = fingerprinted_health_check();
2245        let carrier = HealthCheckHeaderValue {
2246            name: "Authorization".to_owned(),
2247            value: HEALTH_CHECK_SECRET.as_bytes().to_vec(),
2248        };
2249        let protected = Request::from(RequestType::SetProtectedHealthCheck(
2250            SetProtectedHealthCheck {
2251                cluster_id: "app".to_owned(),
2252                config: config.to_owned(),
2253                values: vec![carrier.to_owned()],
2254            },
2255        ));
2256        let worker_request = WorkerRequest::new("id".to_owned(), protected.to_owned());
2257        let legacy = Request::from(RequestType::SetHealthCheck(SetHealthCheck {
2258            cluster_id: "app".to_owned(),
2259            config,
2260        }));
2261        let numeric = format!("{:?}", HEALTH_CHECK_SECRET.as_bytes());
2262        for (what, rendered) in [
2263            ("carrier", format!("{carrier:?}")),
2264            ("carrier pretty", format!("{carrier:#?}")),
2265            ("protected request", format!("{protected:?}")),
2266            ("protected request pretty", format!("{protected:#?}")),
2267            ("worker request", format!("{worker_request:?}")),
2268            ("legacy request", format!("{legacy:?}")),
2269        ] {
2270            assert!(
2271                !rendered.contains(HEALTH_CHECK_SECRET) && !rendered.contains(&numeric),
2272                "{what} Debug leaked a header value: {rendered}"
2273            );
2274            if what.starts_with("carrier") {
2275                assert!(rendered.contains("<protected>"), "{what}: {rendered}");
2276            }
2277        }
2278    }
2279}