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