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 pub use crate::secret::HealthCheckHeaderValue;
16}
17
18pub 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
35impl 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
44impl 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
253const HEALTH_CHECK_HEADER_DEBUG_NAME_MAX: usize = 64;
255
256fn 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
266impl 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
280impl 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 .finish_non_exhaustive()
442 }
443}
444
445impl std::fmt::Debug for command::UpdateHttpListenerConfig {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 let (http_answers, http_answers_count, http_answers_len) =
448 summarize_http_answers(self.http_answers.as_ref());
449 let (answers_count, answers_key_len, answers_value_len) =
450 summarize_string_map(&self.answers);
451
452 f.debug_struct("UpdateHttpListenerConfig")
453 .field("address", &self.address)
454 .field("public_address", &self.public_address)
455 .field(
456 "sticky_name_len",
457 &self.sticky_name.as_ref().map(String::len),
458 )
459 .field("http_answers", &http_answers)
460 .field("http_answers_count", &http_answers_count)
461 .field("http_answers_len", &http_answers_len)
462 .field(
463 "sozu_id_header_len",
464 &self.sozu_id_header.as_ref().map(String::len),
465 )
466 .field("answers_count", &answers_count)
467 .field("answers_key_len", &answers_key_len)
468 .field("answers_value_len", &answers_value_len)
469 .field("forwarded_headers", &self.forwarded_headers)
470 .finish_non_exhaustive()
471 }
472}
473
474impl std::fmt::Debug for command::UpdateHttpsListenerConfig {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 let (http_answers, http_answers_count, http_answers_len) =
477 summarize_http_answers(self.http_answers.as_ref());
478 let alpn_protocols_count = self
479 .alpn_protocols
480 .as_ref()
481 .map(|protocols| protocols.values.len());
482 let alpn_protocols_len = self
483 .alpn_protocols
484 .as_ref()
485 .map(|protocols| total_string_len(&protocols.values));
486 let (answers_count, answers_key_len, answers_value_len) =
487 summarize_string_map(&self.answers);
488
489 f.debug_struct("UpdateHttpsListenerConfig")
490 .field("address", &self.address)
491 .field("public_address", &self.public_address)
492 .field(
493 "sticky_name_len",
494 &self.sticky_name.as_ref().map(String::len),
495 )
496 .field("http_answers", &http_answers)
497 .field("http_answers_count", &http_answers_count)
498 .field("http_answers_len", &http_answers_len)
499 .field("alpn_protocols_count", &alpn_protocols_count)
500 .field("alpn_protocols_len", &alpn_protocols_len)
501 .field(
502 "sozu_id_header_len",
503 &self.sozu_id_header.as_ref().map(String::len),
504 )
505 .field("answers_count", &answers_count)
506 .field("answers_key_len", &answers_key_len)
507 .field("answers_value_len", &answers_value_len)
508 .field("forwarded_headers", &self.forwarded_headers)
509 .field("client_auth_policy", &self.client_auth_policy)
510 .finish_non_exhaustive()
511 }
512}
513
514impl std::fmt::Debug for command::ClientAuthPolicy {
516 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517 f.debug_struct("ClientAuthPolicy")
518 .field("client_auth", &self.client_auth)
519 .field(
520 "client_ca_certificates_count",
521 &self.client_ca_certificates.len(),
522 )
523 .field(
524 "client_ca_certificates_len",
525 &total_string_len(&self.client_ca_certificates),
526 )
527 .field("client_ca_crls_count", &self.client_ca_crls.len())
528 .field(
529 "client_ca_crls_len",
530 &total_string_len(&self.client_ca_crls),
531 )
532 .finish()
533 }
534}
535
536impl std::fmt::Debug for command::HttpsListenerConfig {
537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
539 let certificate_len = self.certificate.as_ref().map(String::len);
540 let certificate_chain_len = total_string_len(&self.certificate_chain);
541 let key = self.key.as_ref().map(|_| "[redacted]");
542 let key_len = self.key.as_ref().map(String::len);
543 let (http_answers, http_answers_count, http_answers_len) =
544 summarize_http_answers(self.http_answers.as_ref());
545 let sozu_id_header_len = self.sozu_id_header.as_ref().map(String::len);
546 let (answers_count, answers_key_len, answers_value_len) =
547 summarize_string_map(&self.answers);
548
549 f.debug_struct("HttpsListenerConfig")
550 .field("address", &self.address)
551 .field("interface", &self.interface)
552 .field("public_address", &self.public_address)
553 .field("expect_proxy", &self.expect_proxy)
554 .field("sticky_name_len", &self.sticky_name.len())
555 .field("front_timeout", &self.front_timeout)
556 .field("back_timeout", &self.back_timeout)
557 .field("connect_timeout", &self.connect_timeout)
558 .field("request_timeout", &self.request_timeout)
559 .field("active", &self.active)
560 .field("versions_count", &self.versions.len())
561 .field("cipher_list_count", &self.cipher_list.len())
562 .field("cipher_list_len", &total_string_len(&self.cipher_list))
563 .field("cipher_suites_count", &self.cipher_suites.len())
564 .field("cipher_suites_len", &total_string_len(&self.cipher_suites))
565 .field(
566 "signature_algorithms_count",
567 &self.signature_algorithms.len(),
568 )
569 .field(
570 "signature_algorithms_len",
571 &total_string_len(&self.signature_algorithms),
572 )
573 .field("groups_list_count", &self.groups_list.len())
574 .field("groups_list_len", &total_string_len(&self.groups_list))
575 .field("certificate", &certificate)
576 .field("certificate_len", &certificate_len)
577 .field("certificate_chain", &"[redacted]")
578 .field("certificate_chain_count", &self.certificate_chain.len())
579 .field("certificate_chain_len", &certificate_chain_len)
580 .field("key", &key)
581 .field("key_len", &key_len)
582 .field("send_tls13_tickets", &self.send_tls13_tickets)
583 .field("http_answers", &http_answers)
584 .field("http_answers_count", &http_answers_count)
585 .field("http_answers_len", &http_answers_len)
586 .field("alpn_protocols_count", &self.alpn_protocols.len())
587 .field(
588 "alpn_protocols_len",
589 &total_string_len(&self.alpn_protocols),
590 )
591 .field(
592 "h2_max_rst_stream_per_window",
593 &self.h2_max_rst_stream_per_window,
594 )
595 .field("h2_max_ping_per_window", &self.h2_max_ping_per_window)
596 .field(
597 "h2_max_settings_per_window",
598 &self.h2_max_settings_per_window,
599 )
600 .field(
601 "h2_max_empty_data_per_window",
602 &self.h2_max_empty_data_per_window,
603 )
604 .field(
605 "h2_max_continuation_frames",
606 &self.h2_max_continuation_frames,
607 )
608 .field("h2_max_glitch_count", &self.h2_max_glitch_count)
609 .field(
610 "h2_initial_connection_window",
611 &self.h2_initial_connection_window,
612 )
613 .field("h2_max_concurrent_streams", &self.h2_max_concurrent_streams)
614 .field("h2_stream_shrink_ratio", &self.h2_stream_shrink_ratio)
615 .field(
616 "h2_max_rst_stream_lifetime",
617 &self.h2_max_rst_stream_lifetime,
618 )
619 .field(
620 "h2_max_rst_stream_abusive_lifetime",
621 &self.h2_max_rst_stream_abusive_lifetime,
622 )
623 .field(
624 "h2_max_rst_stream_emitted_lifetime",
625 &self.h2_max_rst_stream_emitted_lifetime,
626 )
627 .field("h2_max_header_list_size", &self.h2_max_header_list_size)
628 .field("strict_sni_binding", &self.strict_sni_binding)
629 .field("disable_http11", &self.disable_http11)
630 .field(
631 "h2_stream_idle_timeout_seconds",
632 &self.h2_stream_idle_timeout_seconds,
633 )
634 .field("h2_max_header_table_size", &self.h2_max_header_table_size)
635 .field(
636 "h2_graceful_shutdown_deadline_seconds",
637 &self.h2_graceful_shutdown_deadline_seconds,
638 )
639 .field(
640 "h2_max_window_update_stream0_per_window",
641 &self.h2_max_window_update_stream0_per_window,
642 )
643 .field("sozu_id_header_len", &sozu_id_header_len)
644 .field("answers_count", &answers_count)
645 .field("answers_key_len", &answers_key_len)
646 .field("answers_value_len", &answers_value_len)
647 .field("elide_x_real_ip", &self.elide_x_real_ip)
648 .field("send_x_real_ip", &self.send_x_real_ip)
649 .field("forwarded_headers", &self.forwarded_headers)
650 .field("hsts", &self.hsts)
651 .field("h2_max_header_fields", &self.h2_max_header_fields)
652 .field("h2_stream_refusal_percent", &self.h2_stream_refusal_percent)
653 .field("client_auth", &self.client_auth)
654 .field(
655 "client_ca_certificates_count",
656 &self.client_ca_certificates.len(),
657 )
658 .field(
659 "client_ca_certificates_len",
660 &total_string_len(&self.client_ca_certificates),
661 )
662 .field("client_ca_crls_count", &self.client_ca_crls.len())
663 .field(
664 "client_ca_crls_len",
665 &total_string_len(&self.client_ca_crls),
666 )
667 .finish()
668 }
669}
670
671impl std::fmt::Debug for command::QueryCertificatesFilters {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 f.debug_struct("QueryCertificatesFilters")
674 .field("domain_len", &self.domain.as_ref().map(String::len))
675 .field(
676 "fingerprint_len",
677 &self.fingerprint.as_ref().map(String::len),
678 )
679 .finish()
680 }
681}
682
683impl std::fmt::Debug for command::CertificateSummary {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 f.debug_struct("CertificateSummary")
686 .field("domain_len", &self.domain.len())
687 .field("fingerprint_len", &self.fingerprint.len())
688 .finish()
689 }
690}
691
692impl std::fmt::Debug for command::CertificatesByAddress {
693 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694 let domain_len = self
695 .certificate_summaries
696 .iter()
697 .map(|summary| summary.domain.len())
698 .fold(0usize, usize::saturating_add);
699 let fingerprint_len = self
700 .certificate_summaries
701 .iter()
702 .map(|summary| summary.fingerprint.len())
703 .fold(0usize, usize::saturating_add);
704
705 f.debug_struct("CertificatesByAddress")
706 .field("address", &self.address)
707 .field("certificates_count", &self.certificate_summaries.len())
708 .field("domain_len", &domain_len)
709 .field("fingerprint_len", &fingerprint_len)
710 .finish()
711 }
712}
713
714impl std::fmt::Debug for command::ListOfCertificatesByAddress {
715 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716 let certificates_count = self
717 .certificates
718 .iter()
719 .map(|entry| entry.certificate_summaries.len())
720 .fold(0usize, usize::saturating_add);
721 let domain_len = self
722 .certificates
723 .iter()
724 .flat_map(|entry| &entry.certificate_summaries)
725 .map(|summary| summary.domain.len())
726 .fold(0usize, usize::saturating_add);
727 let fingerprint_len = self
728 .certificates
729 .iter()
730 .flat_map(|entry| &entry.certificate_summaries)
731 .map(|summary| summary.fingerprint.len())
732 .fold(0usize, usize::saturating_add);
733
734 f.debug_struct("ListOfCertificatesByAddress")
735 .field("listeners_count", &self.certificates.len())
736 .field("certificates_count", &certificates_count)
737 .field("domain_len", &domain_len)
738 .field("fingerprint_len", &fingerprint_len)
739 .finish()
740 }
741}
742
743impl std::fmt::Debug for command::CertificatesWithFingerprints {
744 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
745 let fingerprint_len = total_string_len(self.certs.keys());
746 let names_count = self
747 .certs
748 .values()
749 .map(|certificate| certificate.names.len())
750 .fold(0usize, usize::saturating_add);
751 let names_len = self
752 .certs
753 .values()
754 .flat_map(|certificate| &certificate.names)
755 .map(String::len)
756 .fold(0usize, usize::saturating_add);
757
758 f.debug_struct("CertificatesWithFingerprints")
759 .field("certificates_count", &self.certs.len())
760 .field("fingerprint_len", &fingerprint_len)
761 .field("names_count", &names_count)
762 .field("names_len", &names_len)
763 .finish()
764 }
765}
766
767fn response_content_kind(content: Option<&command::ResponseContent>) -> Option<&'static str> {
768 use command::response_content::ContentType;
769
770 content
771 .and_then(|response| response.content_type.as_ref())
772 .map(|content_type| match content_type {
773 ContentType::Workers(_) => "workers",
774 ContentType::Metrics(_) => "metrics",
775 ContentType::WorkerResponses(_) => "worker_responses",
776 ContentType::Event(_) => "event",
777 ContentType::FrontendList(_) => "frontend_list",
778 ContentType::ListenersList(_) => "listeners_list",
779 ContentType::WorkerMetrics(_) => "worker_metrics",
780 ContentType::AvailableMetrics(_) => "available_metrics",
781 ContentType::Clusters(_) => "clusters",
782 ContentType::ClusterHashes(_) => "cluster_hashes",
783 ContentType::CertificatesByAddress(_) => "certificates_by_address",
784 ContentType::CertificatesWithFingerprints(_) => "certificates_with_fingerprints",
785 ContentType::RequestCounts(_) => "request_counts",
786 ContentType::MaxConnectionsPerIpLimit(_) => "max_connections_per_ip_limit",
787 ContentType::MaxConnectionsPerSubnetLimit(_) => "max_connections_per_subnet_limit",
788 ContentType::HealthChecksList(_) => "health_checks_list",
789 ContentType::MetricDetailStatus(_) => "metric_detail_status",
790 ContentType::WorkerMetricDetailStatus(_) => "worker_metric_detail_status",
791 ContentType::ClosedSessions(_) => "closed_sessions",
792 ContentType::ProtectedHealthCheckReply(_) => "protected_health_check_reply",
793 })
794}
795
796impl std::fmt::Debug for command::ResponseContent {
797 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798 f.debug_struct("ResponseContent")
799 .field("content_type", &response_content_kind(Some(self)))
800 .finish_non_exhaustive()
801 }
802}
803
804impl std::fmt::Debug for command::WorkerResponses {
805 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
806 f.debug_struct("WorkerResponses")
807 .field("workers_count", &self.map.len())
808 .field("worker_ids_len", &total_string_len(self.map.keys()))
809 .finish_non_exhaustive()
810 }
811}
812
813impl std::fmt::Debug for command::WorkerRequest {
814 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
815 f.debug_struct("WorkerRequest")
816 .field("id_len", &self.id.len())
817 .field("content", &self.content)
818 .finish()
819 }
820}
821
822impl std::fmt::Debug for command::WorkerResponse {
823 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824 f.debug_struct("WorkerResponse")
825 .field("id_len", &self.id.len())
826 .field("status", &self.status)
827 .field("message_len", &self.message.len())
828 .field(
829 "content_type",
830 &response_content_kind(self.content.as_ref()),
831 )
832 .finish()
833 }
834}
835
836impl AggregatedMetrics {
837 pub fn merge_metrics(&mut self) {
843 let workers = std::mem::take(&mut self.workers);
845
846 for (_worker_id, worker) in workers {
847 for (metric_name, new_value) in worker.proxy {
848 if new_value.is_mergeable() {
849 self.proxying
850 .entry(metric_name)
851 .and_modify(|old_value| old_value.merge(&new_value))
852 .or_insert(new_value);
853 }
854 }
855
856 for (cluster_id, mut cluster_metrics) in worker.clusters {
857 for (metric_name, new_value) in cluster_metrics.cluster {
858 if new_value.is_mergeable() {
859 let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
860
861 cluster
862 .cluster
863 .entry(metric_name)
864 .and_modify(|old_value| old_value.merge(&new_value))
865 .or_insert(new_value);
866 }
867 }
868
869 for backend in cluster_metrics.backends.drain(..) {
870 for (metric_name, new_value) in backend.metrics {
871 if new_value.is_mergeable() {
872 let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
873
874 let found_backend = cluster
875 .backends
876 .iter_mut()
877 .find(|present| present.backend_id == backend.backend_id);
878
879 if let Some(existing_backend) = found_backend {
880 let _ = existing_backend
881 .metrics
882 .entry(metric_name)
883 .and_modify(|old_value| old_value.merge(&new_value))
884 .or_insert(new_value);
885 } else {
886 cluster.backends.push(BackendMetrics {
887 backend_id: backend.backend_id.clone(),
888 metrics: BTreeMap::from([(metric_name, new_value)]),
889 });
890 };
891 }
892 }
893 }
894 }
895 }
896 }
897}
898
899impl FilteredMetrics {
900 pub fn merge(&mut self, right: &Self) {
901 match (&self.inner, &right.inner) {
902 (Some(Inner::Gauge(a)), Some(Inner::Gauge(b))) => {
903 *self = Self {
904 inner: Some(Inner::Gauge(a + b)),
905 };
906 }
907 (Some(Inner::Count(a)), Some(Inner::Count(b))) => {
908 *self = Self {
909 inner: Some(Inner::Count(a + b)),
910 };
911 }
912 (Some(Inner::Histogram(a)), Some(Inner::Histogram(b))) => {
913 let longest_len = a.buckets.len().max(b.buckets.len());
914
915 let mut a_count = 0;
916 let mut b_count = 0;
917 let buckets = (0..longest_len)
918 .map(|i| {
919 if let Some(a_bucket) = a.buckets.get(i) {
920 a_count = a_bucket.count;
921 }
922 if let Some(b_bucket) = b.buckets.get(i) {
923 b_count = b_bucket.count;
924 }
925 Bucket {
926 le: (1 << i) - 1, count: a_count + b_count,
928 }
929 })
930 .collect();
931
932 *self = Self {
933 inner: Some(Inner::Histogram(FilteredHistogram {
934 count: a.count + b.count,
935 sum: a.sum + b.sum,
936 buckets,
937 })),
938 };
939 }
940 (Some(Inner::Percentiles(a)), Some(Inner::Percentiles(b))) => {
941 *self = Self {
951 inner: Some(Inner::Percentiles(Percentiles {
952 samples: a.samples + b.samples,
953 p_50: a.p_50.max(b.p_50),
954 p_90: a.p_90.max(b.p_90),
955 p_99: a.p_99.max(b.p_99),
956 p_99_9: a.p_99_9.max(b.p_99_9),
957 p_99_99: a.p_99_99.max(b.p_99_99),
958 p_99_999: a.p_99_999.max(b.p_99_999),
959 p_100: a.p_100.max(b.p_100),
960 sum: a.sum + b.sum,
961 })),
962 };
963 }
964 _ => {}
965 }
966 }
967
968 fn is_mergeable(&self) -> bool {
969 match &self.inner {
970 Some(Inner::Gauge(_))
971 | Some(Inner::Count(_))
972 | Some(Inner::Histogram(_))
973 | Some(Inner::Percentiles(_)) => true,
974 Some(Inner::Time(_)) | Some(Inner::TimeSerie(_)) | None => false,
976 }
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use std::{
983 collections::{BTreeMap, HashMap},
984 net::SocketAddr,
985 };
986
987 use crate::{certificate::Fingerprint, state::ConfigState};
988
989 use super::AggregatedMetrics;
990 use super::command::{
991 AddCertificate, AlpnProtocols, Bucket, CertificateAndKey, CertificateSummary,
992 CertificatesByAddress, CertificatesWithFingerprints, Cluster, ClusterMetrics,
993 CustomHttpAnswers, FilteredHistogram, FilteredMetrics, Header, HeaderMatchKind,
994 HeaderPosition, HeaderPredicate, HttpListenerConfig, HttpsListenerConfig,
995 ListOfCertificatesByAddress, PathRule, PathRuleKind, Percentiles, QueryCertificatesFilters,
996 RedirectPolicy, RedirectScheme, RemoveCertificate, ReplaceCertificate, Request,
997 RequestHttpFrontend, RequestTcpFrontend, ResponseContent, ResponseStatus, RulePosition,
998 TlsVersion, UpdateHttpListenerConfig, UpdateHttpsListenerConfig, WorkerMetrics,
999 WorkerRequest, WorkerResponse, WorkerResponses, filtered_metrics::Inner,
1000 request::RequestType, response_content::ContentType,
1001 };
1002
1003 #[test]
1004 fn request_debug_redacts_string_variant_payloads_directly_and_when_nested() {
1005 const PATH_SECRET: &str = "REQUEST_SAVE_STATE_PATH_SECRET_SENTINEL";
1006
1007 let request_type = RequestType::SaveState(format!("{PATH_SECRET}{}", "x".repeat(4096)));
1008 let direct_output = format!("{request_type:?}");
1009 let request = Request::from(request_type);
1010 let worker_request = WorkerRequest {
1011 id: "safe-request-id".to_owned(),
1012 content: request.clone(),
1013 };
1014 let outputs = [
1015 direct_output,
1016 format!("{request:?}"),
1017 format!("{worker_request:?}"),
1018 ];
1019
1020 for output in outputs {
1021 assert!(
1022 !output.contains(PATH_SECRET),
1023 "Request Debug leaked its string variant payload: {output}"
1024 );
1025 assert!(
1026 output.contains("SaveState"),
1027 "Request Debug omitted the bounded request kind: {output}"
1028 );
1029 assert!(
1030 output.len() <= 256,
1031 "Request Debug output is not bounded: {} bytes",
1032 output.len()
1033 );
1034 }
1035 }
1036
1037 #[test]
1038 fn listener_and_patch_debug_redacts_user_controlled_material() {
1039 const HTTP_STICKY_SECRET: &str = "HTTP_LISTENER_STICKY_SECRET_SENTINEL";
1040 const HTTP_LEGACY_ANSWER_SECRET: &str = "HTTP_LISTENER_LEGACY_ANSWER_SECRET_SENTINEL";
1041 const HTTP_HEADER_SECRET: &str = "HTTP_LISTENER_HEADER_SECRET_SENTINEL";
1042 const HTTP_ANSWER_KEY_SECRET: &str = "HTTP_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1043 const HTTP_ANSWER_BODY_SECRET: &str = "HTTP_LISTENER_ANSWER_BODY_SECRET_SENTINEL";
1044 const HTTP_PATCH_STICKY_SECRET: &str = "HTTP_PATCH_STICKY_SECRET_SENTINEL";
1045 const HTTP_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTP_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1046 const HTTP_PATCH_HEADER_SECRET: &str = "HTTP_PATCH_HEADER_SECRET_SENTINEL";
1047 const HTTP_PATCH_ANSWER_KEY_SECRET: &str = "HTTP_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1048 const HTTP_PATCH_ANSWER_BODY_SECRET: &str = "HTTP_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1049 const HTTPS_PATCH_STICKY_SECRET: &str = "HTTPS_PATCH_STICKY_SECRET_SENTINEL";
1050 const HTTPS_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTPS_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1051 const HTTPS_PATCH_ALPN_SECRET: &str = "HTTPS_PATCH_ALPN_SECRET_SENTINEL";
1052 const HTTPS_PATCH_HEADER_SECRET: &str = "HTTPS_PATCH_HEADER_SECRET_SENTINEL";
1053 const HTTPS_PATCH_ANSWER_KEY_SECRET: &str = "HTTPS_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1054 const HTTPS_PATCH_ANSWER_BODY_SECRET: &str = "HTTPS_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1055
1056 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1057 let http_address: SocketAddr = "127.0.0.1:8080"
1058 .parse()
1059 .expect("test HTTP listener address must parse");
1060 let http_listener = HttpListenerConfig {
1061 address: http_address.into(),
1062 sticky_name: long_value(HTTP_STICKY_SECRET),
1063 http_answers: Some(CustomHttpAnswers {
1064 answer_503: Some(long_value(HTTP_LEGACY_ANSWER_SECRET)),
1065 ..Default::default()
1066 }),
1067 sozu_id_header: Some(long_value(HTTP_HEADER_SECRET)),
1068 answers: BTreeMap::from([(
1069 long_value(HTTP_ANSWER_KEY_SECRET),
1070 long_value(HTTP_ANSWER_BODY_SECRET),
1071 )]),
1072 ..Default::default()
1073 };
1074 let http_request = Request::from(RequestType::AddHttpListener(http_listener.clone()));
1075 let http_worker_request = WorkerRequest {
1076 id: "safe-add-http-listener-id".to_owned(),
1077 content: http_request.clone(),
1078 };
1079
1080 let http_patch = UpdateHttpListenerConfig {
1081 address: http_address.into(),
1082 sticky_name: Some(long_value(HTTP_PATCH_STICKY_SECRET)),
1083 http_answers: Some(CustomHttpAnswers {
1084 answer_503: Some(long_value(HTTP_PATCH_LEGACY_ANSWER_SECRET)),
1085 ..Default::default()
1086 }),
1087 sozu_id_header: Some(long_value(HTTP_PATCH_HEADER_SECRET)),
1088 answers: BTreeMap::from([(
1089 long_value(HTTP_PATCH_ANSWER_KEY_SECRET),
1090 long_value(HTTP_PATCH_ANSWER_BODY_SECRET),
1091 )]),
1092 ..Default::default()
1093 };
1094 let http_patch_request = Request::from(RequestType::UpdateHttpListener(http_patch.clone()));
1095 let http_patch_worker_request = WorkerRequest {
1096 id: "safe-update-http-listener-id".to_owned(),
1097 content: http_patch_request.clone(),
1098 };
1099
1100 let https_address: SocketAddr = "127.0.0.1:8443"
1101 .parse()
1102 .expect("test HTTPS listener address must parse");
1103 let https_patch = UpdateHttpsListenerConfig {
1104 address: https_address.into(),
1105 sticky_name: Some(long_value(HTTPS_PATCH_STICKY_SECRET)),
1106 http_answers: Some(CustomHttpAnswers {
1107 answer_503: Some(long_value(HTTPS_PATCH_LEGACY_ANSWER_SECRET)),
1108 ..Default::default()
1109 }),
1110 alpn_protocols: Some(AlpnProtocols {
1111 values: vec![long_value(HTTPS_PATCH_ALPN_SECRET)],
1112 }),
1113 sozu_id_header: Some(long_value(HTTPS_PATCH_HEADER_SECRET)),
1114 answers: BTreeMap::from([(
1115 long_value(HTTPS_PATCH_ANSWER_KEY_SECRET),
1116 long_value(HTTPS_PATCH_ANSWER_BODY_SECRET),
1117 )]),
1118 ..Default::default()
1119 };
1120 let https_patch_request =
1121 Request::from(RequestType::UpdateHttpsListener(https_patch.clone()));
1122 let https_patch_worker_request = WorkerRequest {
1123 id: "safe-update-https-listener-id".to_owned(),
1124 content: https_patch_request.clone(),
1125 };
1126
1127 let debug_outputs = [
1128 ("HttpListenerConfig", format!("{http_listener:?}")),
1129 ("AddHttpListener Request", format!("{http_request:?}")),
1130 (
1131 "AddHttpListener WorkerRequest",
1132 format!("{http_worker_request:?}"),
1133 ),
1134 ("UpdateHttpListenerConfig", format!("{http_patch:?}")),
1135 (
1136 "UpdateHttpListener Request",
1137 format!("{http_patch_request:?}"),
1138 ),
1139 (
1140 "UpdateHttpListener WorkerRequest",
1141 format!("{http_patch_worker_request:?}"),
1142 ),
1143 ("UpdateHttpsListenerConfig", format!("{https_patch:?}")),
1144 (
1145 "UpdateHttpsListener Request",
1146 format!("{https_patch_request:?}"),
1147 ),
1148 (
1149 "UpdateHttpsListener WorkerRequest",
1150 format!("{https_patch_worker_request:?}"),
1151 ),
1152 ];
1153 let secrets = [
1154 HTTP_STICKY_SECRET,
1155 HTTP_LEGACY_ANSWER_SECRET,
1156 HTTP_HEADER_SECRET,
1157 HTTP_ANSWER_KEY_SECRET,
1158 HTTP_ANSWER_BODY_SECRET,
1159 HTTP_PATCH_STICKY_SECRET,
1160 HTTP_PATCH_LEGACY_ANSWER_SECRET,
1161 HTTP_PATCH_HEADER_SECRET,
1162 HTTP_PATCH_ANSWER_KEY_SECRET,
1163 HTTP_PATCH_ANSWER_BODY_SECRET,
1164 HTTPS_PATCH_STICKY_SECRET,
1165 HTTPS_PATCH_LEGACY_ANSWER_SECRET,
1166 HTTPS_PATCH_ALPN_SECRET,
1167 HTTPS_PATCH_HEADER_SECRET,
1168 HTTPS_PATCH_ANSWER_KEY_SECRET,
1169 HTTPS_PATCH_ANSWER_BODY_SECRET,
1170 ];
1171 let mut leaks = Vec::new();
1172 for (label, output) in &debug_outputs {
1173 for secret in secrets {
1174 if output.contains(secret) {
1175 leaks.push(format!("{label}:{secret}"));
1176 }
1177 }
1178 }
1179 assert!(
1180 leaks.is_empty(),
1181 "listener Debug leaked secret markers through {}",
1182 leaks.join(", ")
1183 );
1184
1185 let expected_metadata = [
1186 (0, "sticky_name_len: 4132"),
1187 (0, "http_answers_count: Some(1)"),
1188 (0, "answers_count: 1"),
1189 (3, "sticky_name_len: Some(4129)"),
1190 (3, "http_answers_count: Some(1)"),
1191 (3, "answers_count: 1"),
1192 (6, "sticky_name_len: Some(4130)"),
1193 (6, "http_answers_count: Some(1)"),
1194 (6, "alpn_protocols_count: Some(1)"),
1195 (6, "answers_count: 1"),
1196 ];
1197 for (output_index, safe_metadata) in expected_metadata {
1198 assert!(
1199 debug_outputs[output_index].1.contains(safe_metadata),
1200 "{} Debug omitted safe metadata {safe_metadata}: {}",
1201 debug_outputs[output_index].0,
1202 debug_outputs[output_index].1,
1203 );
1204 }
1205 for (label, output) in debug_outputs {
1206 assert!(
1207 output.len() <= 4096,
1208 "{label} Debug output is not bounded: {} bytes",
1209 output.len()
1210 );
1211 }
1212 }
1213
1214 #[test]
1215 fn https_listener_debug_redacts_user_controlled_material() {
1216 const STICKY_NAME_SECRET: &str = "HTTPS_LISTENER_STICKY_NAME_SECRET_SENTINEL";
1217 const CIPHER_LIST_SECRET: &str = "HTTPS_LISTENER_CIPHER_LIST_SECRET_SENTINEL";
1218 const CIPHER_SUITES_SECRET: &str = "HTTPS_LISTENER_CIPHER_SUITES_SECRET_SENTINEL";
1219 const SIGNATURE_ALGORITHMS_SECRET: &str =
1220 "HTTPS_LISTENER_SIGNATURE_ALGORITHMS_SECRET_SENTINEL";
1221 const GROUPS_LIST_SECRET: &str = "HTTPS_LISTENER_GROUPS_LIST_SECRET_SENTINEL";
1222 const CERTIFICATE_SECRET: &str = "HTTPS_LISTENER_CERTIFICATE_SECRET_SENTINEL";
1223 const CHAIN_SECRET: &str = "HTTPS_LISTENER_CHAIN_SECRET_SENTINEL";
1224 const KEY_SECRET: &str = "HTTPS_LISTENER_KEY_SECRET_SENTINEL";
1225 const HTTP_ANSWER_SECRET: &str = "HTTPS_LISTENER_HTTP_ANSWER_SECRET_SENTINEL";
1226 const ALPN_SECRET: &str = "HTTPS_LISTENER_ALPN_SECRET_SENTINEL";
1227 const SOZU_ID_HEADER_SECRET: &str = "HTTPS_LISTENER_SOZU_ID_HEADER_SECRET_SENTINEL";
1228 const ANSWER_KEY_SECRET: &str = "HTTPS_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1229 const ANSWER_VALUE_SECRET: &str = "HTTPS_LISTENER_ANSWER_VALUE_SECRET_SENTINEL";
1230
1231 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1232 let address: SocketAddr = "127.0.0.1:8443"
1233 .parse()
1234 .expect("test HTTPS listener address must parse");
1235 let public_address: SocketAddr = "203.0.113.1:443"
1236 .parse()
1237 .expect("test public HTTPS listener address must parse");
1238 let listener = HttpsListenerConfig {
1239 address: address.into(),
1240 public_address: Some(public_address.into()),
1241 sticky_name: long_value(STICKY_NAME_SECRET),
1242 versions: vec![TlsVersion::TlsV13 as i32],
1243 cipher_list: vec![long_value(CIPHER_LIST_SECRET)],
1244 cipher_suites: vec![long_value(CIPHER_SUITES_SECRET)],
1245 signature_algorithms: vec![long_value(SIGNATURE_ALGORITHMS_SECRET)],
1246 groups_list: vec![long_value(GROUPS_LIST_SECRET)],
1247 certificate: Some(long_value(CERTIFICATE_SECRET)),
1248 certificate_chain: vec![long_value(CHAIN_SECRET)],
1249 key: Some(long_value(KEY_SECRET)),
1250 http_answers: Some(CustomHttpAnswers {
1251 answer_404: Some(long_value(HTTP_ANSWER_SECRET)),
1252 ..Default::default()
1253 }),
1254 alpn_protocols: vec![long_value(ALPN_SECRET)],
1255 sozu_id_header: Some(long_value(SOZU_ID_HEADER_SECRET)),
1256 answers: BTreeMap::from([(
1257 long_value(ANSWER_KEY_SECRET),
1258 long_value(ANSWER_VALUE_SECRET),
1259 )]),
1260 ..Default::default()
1261 };
1262 let add_https_listener = RequestType::AddHttpsListener(listener.clone());
1263 let request = Request::from(add_https_listener.clone());
1264 let worker_request = WorkerRequest {
1265 id: "safe-add-https-listener-id".to_owned(),
1266 content: request.clone(),
1267 };
1268 let state = ConfigState {
1269 https_listeners: BTreeMap::from([(address.into(), listener.clone())]),
1270 ..Default::default()
1271 };
1272 let debug_outputs = [
1273 ("HttpsListenerConfig", format!("{listener:?}")),
1274 (
1275 "RequestType::AddHttpsListener",
1276 format!("{add_https_listener:?}"),
1277 ),
1278 ("Request", format!("{request:?}")),
1279 ("WorkerRequest", format!("{worker_request:?}")),
1280 ("ConfigState", format!("{state:?}")),
1281 ];
1282 let secrets = [
1283 STICKY_NAME_SECRET,
1284 CIPHER_LIST_SECRET,
1285 CIPHER_SUITES_SECRET,
1286 SIGNATURE_ALGORITHMS_SECRET,
1287 GROUPS_LIST_SECRET,
1288 CERTIFICATE_SECRET,
1289 CHAIN_SECRET,
1290 KEY_SECRET,
1291 HTTP_ANSWER_SECRET,
1292 ALPN_SECRET,
1293 SOZU_ID_HEADER_SECRET,
1294 ANSWER_KEY_SECRET,
1295 ANSWER_VALUE_SECRET,
1296 ];
1297 let mut leaks = Vec::new();
1298 for (label, output) in &debug_outputs {
1299 for secret in secrets {
1300 if output.contains(secret) {
1301 leaks.push(format!("{label}:{secret}"));
1302 }
1303 }
1304 }
1305 assert!(
1306 leaks.is_empty(),
1307 "HTTPS listener Debug leaked secret markers through {}",
1308 leaks.join(", ")
1309 );
1310
1311 let direct_output = &debug_outputs[0].1;
1312 let expected_metadata = [
1313 format!("sticky_name_len: {}", long_value(STICKY_NAME_SECRET).len()),
1314 "versions_count: 1".to_owned(),
1315 "cipher_list_count: 1".to_owned(),
1316 format!("cipher_list_len: {}", long_value(CIPHER_LIST_SECRET).len()),
1317 "cipher_suites_count: 1".to_owned(),
1318 "signature_algorithms_count: 1".to_owned(),
1319 "groups_list_count: 1".to_owned(),
1320 "certificate: Some(\"[redacted]\")".to_owned(),
1321 format!(
1322 "certificate_len: Some({})",
1323 long_value(CERTIFICATE_SECRET).len()
1324 ),
1325 "certificate_chain: \"[redacted]\"".to_owned(),
1326 "certificate_chain_count: 1".to_owned(),
1327 "key: Some(\"[redacted]\")".to_owned(),
1328 "http_answers_count: Some(1)".to_owned(),
1329 "alpn_protocols_count: 1".to_owned(),
1330 format!(
1331 "sozu_id_header_len: Some({})",
1332 long_value(SOZU_ID_HEADER_SECRET).len()
1333 ),
1334 "answers_count: 1".to_owned(),
1335 ];
1336 for safe_metadata in expected_metadata {
1337 assert!(
1338 direct_output.contains(&safe_metadata),
1339 "HttpsListenerConfig Debug omitted safe metadata {safe_metadata}: {direct_output}"
1340 );
1341 }
1342 for (label, output) in debug_outputs {
1343 assert!(
1344 output.len() <= 4096,
1345 "{label} Debug output is not bounded: {} bytes",
1346 output.len()
1347 );
1348 }
1349 }
1350
1351 #[test]
1352 fn header_debug_redacts_key_and_value() {
1353 const KEY_SECRET: &str = "HEADER_KEY_SECRET_SENTINEL";
1354 const VALUE_SECRET: &str = "HEADER_VALUE_SECRET_SENTINEL";
1355
1356 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1357 let header = Header {
1358 position: HeaderPosition::Unspecified as i32,
1359 key: long_value(KEY_SECRET),
1360 val: long_value(VALUE_SECRET),
1361 };
1362 let output = format!("{header:?}");
1363
1364 for secret in [KEY_SECRET, VALUE_SECRET] {
1365 assert!(
1366 !output.contains(secret),
1367 "Header Debug leaked user-controlled marker {secret}"
1368 );
1369 }
1370 for metadata in [
1371 "position: 0".to_owned(),
1372 format!("key_len: {}", long_value(KEY_SECRET).len()),
1373 format!("val_len: {}", long_value(VALUE_SECRET).len()),
1374 ] {
1375 assert!(
1376 output.contains(&metadata),
1377 "Header Debug omitted safe metadata {metadata}: {output}"
1378 );
1379 }
1380 assert!(
1381 output.len() <= 256,
1382 "Header Debug output is not bounded: {} bytes",
1383 output.len()
1384 );
1385 }
1386
1387 #[test]
1388 fn http_frontend_request_debug_redacts_user_controlled_material() {
1389 const CLUSTER_SECRET: &str = "HTTP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1390 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
1391 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
1392 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
1393 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1394 const TAG_VALUE_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1395 const REDIRECT_SECRET: &str = "HTTP_FRONTEND_REDIRECT_SECRET_SENTINEL";
1396 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
1397 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
1398 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
1399 const HEADER_VALUE_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
1400 const PREDICATE_NAME_SECRET: &str = "HTTP_FRONTEND_PREDICATE_NAME_SECRET_SENTINEL";
1401 const PREDICATE_VALUE_SECRET: &str = "HTTP_FRONTEND_PREDICATE_VALUE_SECRET_SENTINEL";
1402
1403 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1404 let frontend = RequestHttpFrontend {
1405 cluster_id: Some(long_value(CLUSTER_SECRET)),
1406 address: Default::default(),
1407 hostname: long_value(HOSTNAME_SECRET),
1408 path: PathRule {
1409 value: long_value(PATH_SECRET),
1410 kind: PathRuleKind::Prefix as i32,
1411 },
1412 method: Some(long_value(METHOD_SECRET)),
1413 position: RulePosition::Tree as i32,
1414 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1415 redirect: Some(RedirectPolicy::Forward as i32),
1416 required_auth: Some(true),
1417 redirect_scheme: Some(RedirectScheme::UseSame as i32),
1418 redirect_template: Some(long_value(REDIRECT_SECRET)),
1419 rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
1420 rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
1421 rewrite_port: Some(8443),
1422 headers: vec![Header {
1423 position: HeaderPosition::Request as i32,
1424 key: long_value(HEADER_KEY_SECRET),
1425 val: long_value(HEADER_VALUE_SECRET),
1426 }],
1427 hsts: None,
1428 header_predicates: vec![HeaderPredicate {
1429 name: long_value(PREDICATE_NAME_SECRET),
1430 kind: HeaderMatchKind::Exact as i32,
1431 value: Some(long_value(PREDICATE_VALUE_SECRET)),
1432 }],
1433 };
1434 let request_type = RequestType::AddHttpsFrontend(frontend.clone());
1435 let request = Request::from(request_type.clone());
1436 let worker_request = WorkerRequest {
1437 id: "safe-http-frontend-request-id".to_owned(),
1438 content: request.clone(),
1439 };
1440 let debug_outputs = [
1441 ("RequestHttpFrontend", format!("{frontend:?}")),
1442 ("RequestType", format!("{request_type:?}")),
1443 ("Request", format!("{request:?}")),
1444 ("WorkerRequest", format!("{worker_request:?}")),
1445 ];
1446 let secrets = [
1447 CLUSTER_SECRET,
1448 HOSTNAME_SECRET,
1449 PATH_SECRET,
1450 METHOD_SECRET,
1451 TAG_KEY_SECRET,
1452 TAG_VALUE_SECRET,
1453 REDIRECT_SECRET,
1454 REWRITE_HOST_SECRET,
1455 REWRITE_PATH_SECRET,
1456 HEADER_KEY_SECRET,
1457 HEADER_VALUE_SECRET,
1458 PREDICATE_NAME_SECRET,
1459 PREDICATE_VALUE_SECRET,
1460 ];
1461
1462 for (label, output) in &debug_outputs {
1463 for secret in secrets {
1464 assert!(
1465 !output.contains(secret),
1466 "{label} Debug leaked user-controlled marker {secret}: {output}"
1467 );
1468 }
1469 assert!(
1470 output.len() <= 2048,
1471 "{label} Debug output is not bounded: {} bytes",
1472 output.len()
1473 );
1474 }
1475
1476 let direct_output = &debug_outputs[0].1;
1477 let expected_metadata = [
1478 format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
1479 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
1480 format!("path_len: {}", long_value(PATH_SECRET).len()),
1481 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
1482 "tags_count: 1".to_owned(),
1483 "headers_count: 1".to_owned(),
1484 "header_predicates_count: 1".to_owned(),
1485 ];
1486 for safe_metadata in expected_metadata {
1487 assert!(
1488 direct_output.contains(&safe_metadata),
1489 "RequestHttpFrontend Debug omitted safe metadata {safe_metadata}: {direct_output}"
1490 );
1491 }
1492 }
1493
1494 #[test]
1495 fn certificate_query_request_debug_redacts_filters_across_command_wrappers() {
1496 const DOMAIN_SECRET: &str = "QUERY_REQUEST_DOMAIN_SECRET_SENTINEL";
1497 const FINGERPRINT_SECRET: &str = "QUERY_REQUEST_FINGERPRINT_SECRET_SENTINEL";
1498 const REQUEST_ID_SECRET: &str = "QUERY_REQUEST_ID_SECRET_SENTINEL";
1499
1500 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1501 let filters = QueryCertificatesFilters {
1502 domain: Some(long_value(DOMAIN_SECRET)),
1503 fingerprint: Some(long_value(FINGERPRINT_SECRET)),
1504 };
1505 let request_type = RequestType::QueryCertificatesFromWorkers(filters.clone());
1506 let request = Request::from(request_type.clone());
1507 let worker_request = WorkerRequest {
1508 id: long_value(REQUEST_ID_SECRET),
1509 content: request.clone(),
1510 };
1511 let outputs = [
1512 ("QueryCertificatesFilters", format!("{filters:?}")),
1513 ("RequestType", format!("{request_type:?}")),
1514 ("Request", format!("{request:?}")),
1515 ("WorkerRequest", format!("{worker_request:?}")),
1516 ];
1517
1518 for (label, output) in &outputs {
1519 for secret in [DOMAIN_SECRET, FINGERPRINT_SECRET, REQUEST_ID_SECRET] {
1520 assert!(
1521 !output.contains(secret),
1522 "{label} Debug leaked certificate-query marker {secret}: {output}"
1523 );
1524 }
1525 assert!(
1526 output.len() <= 1024,
1527 "{label} Debug output is not bounded: {} bytes",
1528 output.len()
1529 );
1530 }
1531
1532 let filter_output = &outputs[0].1;
1533 for metadata in [
1534 format!("domain_len: Some({})", long_value(DOMAIN_SECRET).len()),
1535 format!(
1536 "fingerprint_len: Some({})",
1537 long_value(FINGERPRINT_SECRET).len()
1538 ),
1539 ] {
1540 assert!(
1541 filter_output.contains(&metadata),
1542 "QueryCertificatesFilters Debug omitted bounded metadata {metadata}: {filter_output}"
1543 );
1544 }
1545 assert!(
1546 outputs[3]
1547 .1
1548 .contains(&format!("id_len: {}", long_value(REQUEST_ID_SECRET).len())),
1549 "WorkerRequest Debug omitted the bounded request-id length: {}",
1550 outputs[3].1
1551 );
1552 }
1553
1554 #[test]
1555 fn silent_close_frontend_request_debug_keeps_redacted_payload() {
1556 const HOSTNAME_SECRET: &str = "SILENT_CLOSE_HOSTNAME_SECRET_SENTINEL";
1557
1558 let frontend = RequestHttpFrontend {
1559 hostname: HOSTNAME_SECRET.to_owned(),
1560 path: PathRule {
1561 value: "/".to_owned(),
1562 kind: PathRuleKind::Prefix as i32,
1563 },
1564 position: RulePosition::Tree as i32,
1565 redirect: Some(RedirectPolicy::SilentClose as i32),
1566 ..Default::default()
1567 };
1568
1569 for (name, request_type) in [
1570 (
1571 "AddHttpFrontendWithSilentClose",
1572 RequestType::AddHttpFrontendWithSilentClose(frontend.clone()),
1573 ),
1574 (
1575 "AddHttpsFrontendWithSilentClose",
1576 RequestType::AddHttpsFrontendWithSilentClose(frontend.clone()),
1577 ),
1578 ] {
1579 let output = format!("{request_type:?}");
1580 assert!(
1581 output.contains(&format!("hostname_len: {}", HOSTNAME_SECRET.len())),
1582 "{name} Debug dropped the frontend payload: {output}"
1583 );
1584 assert!(
1585 !output.contains(HOSTNAME_SECRET),
1586 "{name} Debug leaked the hostname: {output}"
1587 );
1588 }
1589 }
1590
1591 #[test]
1592 fn certificate_query_response_debug_redacts_results_across_worker_wrappers() {
1593 const DOMAIN_SECRET: &str = "QUERY_RESPONSE_DOMAIN_SECRET_SENTINEL";
1594 const FINGERPRINT_SECRET: &str = "QUERY_RESPONSE_FINGERPRINT_SECRET_SENTINEL";
1595 const MAP_KEY_SECRET: &str = "QUERY_RESPONSE_MAP_KEY_SECRET_SENTINEL";
1596 const CERTIFICATE_SECRET: &str = "QUERY_RESPONSE_CERTIFICATE_SECRET_SENTINEL";
1597 const KEY_SECRET: &str = "QUERY_RESPONSE_KEY_SECRET_SENTINEL";
1598 const NAME_SECRET: &str = "QUERY_RESPONSE_NAME_SECRET_SENTINEL";
1599 const WORKER_ID_SECRET: &str = "QUERY_RESPONSE_WORKER_ID_SECRET_SENTINEL";
1600 const RESPONSE_ID_SECRET: &str = "QUERY_RESPONSE_ID_SECRET_SENTINEL";
1601 const MESSAGE_SECRET: &str = "QUERY_RESPONSE_MESSAGE_SECRET_SENTINEL";
1602
1603 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1604 let summary = CertificateSummary {
1605 domain: long_value(DOMAIN_SECRET),
1606 fingerprint: long_value(FINGERPRINT_SECRET),
1607 };
1608 let by_address = CertificatesByAddress {
1609 address: Default::default(),
1610 certificate_summaries: vec![summary.clone()],
1611 };
1612 let list = ListOfCertificatesByAddress {
1613 certificates: vec![by_address.clone()],
1614 };
1615 let worker_content =
1616 ResponseContent::from(ContentType::CertificatesByAddress(list.clone()));
1617 let worker_response = WorkerResponse {
1618 id: long_value(RESPONSE_ID_SECRET),
1619 status: ResponseStatus::Ok as i32,
1620 message: long_value(MESSAGE_SECRET),
1621 content: Some(worker_content.clone()),
1622 };
1623 let worker_responses = WorkerResponses {
1624 map: BTreeMap::from([(long_value(WORKER_ID_SECRET), worker_content.clone())]),
1625 };
1626 let gathered_content =
1627 ResponseContent::from(ContentType::WorkerResponses(worker_responses.clone()));
1628 let main_certificates = CertificatesWithFingerprints {
1629 certs: BTreeMap::from([(
1630 long_value(MAP_KEY_SECRET),
1631 CertificateAndKey {
1632 certificate: long_value(CERTIFICATE_SECRET),
1633 certificate_chain: Vec::new(),
1634 key: long_value(KEY_SECRET),
1635 versions: Vec::new(),
1636 names: vec![long_value(NAME_SECRET)],
1637 },
1638 )]),
1639 };
1640 let main_content = ResponseContent::from(ContentType::CertificatesWithFingerprints(
1641 main_certificates.clone(),
1642 ));
1643 let outputs = [
1644 ("CertificateSummary", format!("{summary:?}")),
1645 ("CertificatesByAddress", format!("{by_address:?}")),
1646 ("ListOfCertificatesByAddress", format!("{list:?}")),
1647 ("worker ResponseContent", format!("{worker_content:?}")),
1648 ("WorkerResponse", format!("{worker_response:?}")),
1649 ("WorkerResponses", format!("{worker_responses:?}")),
1650 ("gathered ResponseContent", format!("{gathered_content:?}")),
1651 (
1652 "CertificatesWithFingerprints",
1653 format!("{main_certificates:?}"),
1654 ),
1655 ("main ResponseContent", format!("{main_content:?}")),
1656 ];
1657 let secrets = [
1658 DOMAIN_SECRET,
1659 FINGERPRINT_SECRET,
1660 MAP_KEY_SECRET,
1661 CERTIFICATE_SECRET,
1662 KEY_SECRET,
1663 NAME_SECRET,
1664 WORKER_ID_SECRET,
1665 RESPONSE_ID_SECRET,
1666 MESSAGE_SECRET,
1667 ];
1668
1669 for (label, output) in &outputs {
1670 for secret in secrets {
1671 assert!(
1672 !output.contains(secret),
1673 "{label} Debug leaked certificate-query marker {secret}: {output}"
1674 );
1675 }
1676 assert!(
1677 output.len() <= 1024,
1678 "{label} Debug output is not bounded: {} bytes",
1679 output.len()
1680 );
1681 }
1682
1683 for metadata in ["listeners_count: 1", "certificates_count: 1"] {
1684 assert!(
1685 outputs[2].1.contains(metadata),
1686 "ListOfCertificatesByAddress Debug omitted bounded metadata {metadata}: {}",
1687 outputs[2].1
1688 );
1689 }
1690 for metadata in ["id_len:", "message_len:", "content_type:"] {
1691 assert!(
1692 outputs[4].1.contains(metadata),
1693 "WorkerResponse Debug omitted bounded metadata {metadata}: {}",
1694 outputs[4].1
1695 );
1696 }
1697 }
1698
1699 #[test]
1700 fn certificate_debug_redacts_pem_material() {
1701 const CERTIFICATE_SECRET: &str = "CERTIFICATE_SECRET_SENTINEL";
1702 const CHAIN_SECRET: &str = "CHAIN_SECRET_SENTINEL";
1703 const KEY_SECRET: &str = "KEY_SECRET_SENTINEL";
1704 const NAME_SECRET: &str = "CERTIFICATE_NAME_SECRET_SENTINEL";
1705
1706 let certificate = CertificateAndKey {
1707 certificate: CERTIFICATE_SECRET.to_owned(),
1708 certificate_chain: vec![CHAIN_SECRET.to_owned()],
1709 key: KEY_SECRET.to_owned(),
1710 versions: vec![i32::MIN; 1_024],
1711 names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1712 };
1713 let add_certificate = AddCertificate {
1714 address: Default::default(),
1715 certificate: certificate.clone(),
1716 expired_at: None,
1717 };
1718 let replace_certificate = ReplaceCertificate {
1719 address: Default::default(),
1720 new_certificate: certificate.clone(),
1721 old_fingerprint: "safe-old-fingerprint".to_owned(),
1722 new_expired_at: None,
1723 };
1724 let add_request = Request::from(RequestType::AddCertificate(add_certificate.clone()));
1725 let replace_request =
1726 Request::from(RequestType::ReplaceCertificate(replace_certificate.clone()));
1727 let add_worker_request = WorkerRequest {
1728 id: "safe-add-request-id".to_owned(),
1729 content: add_request.clone(),
1730 };
1731 let replace_worker_request = WorkerRequest {
1732 id: "safe-replace-request-id".to_owned(),
1733 content: replace_request.clone(),
1734 };
1735 let state_address: SocketAddr = "127.0.0.1:443"
1736 .parse()
1737 .expect("test certificate address must parse");
1738 let state = ConfigState {
1739 certificates: HashMap::from([(
1740 state_address,
1741 HashMap::from([(Fingerprint(vec![0; 32]), certificate.clone())]),
1742 )]),
1743 ..Default::default()
1744 };
1745
1746 let debug_outputs = [
1747 format!("{certificate:?}"),
1748 format!("{add_certificate:?}"),
1749 format!("{replace_certificate:?}"),
1750 format!("{add_request:?}"),
1751 format!("{replace_request:?}"),
1752 format!("{add_worker_request:?}"),
1753 format!("{replace_worker_request:?}"),
1754 ];
1755 let state_debug = format!("{state:?}");
1756
1757 for output in debug_outputs {
1758 for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1759 assert!(
1760 !output.contains(secret),
1761 "debug output leaked secret marker {secret}: {output}"
1762 );
1763 }
1764 assert!(
1765 output.contains("[redacted]"),
1766 "debug output must make redaction explicit: {output}"
1767 );
1768 assert!(
1769 output.contains("names_count: 1") && output.contains("names_len: 4128"),
1770 "debug output must retain bounded certificate-name metadata: {output}"
1771 );
1772 assert!(
1773 output.contains("versions_count: 1024"),
1774 "debug output must retain bounded TLS-version metadata: {output}"
1775 );
1776 assert!(
1777 output.len() <= 4096,
1778 "certificate Debug output is not bounded: {} bytes",
1779 output.len()
1780 );
1781 }
1782
1783 for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1784 assert!(
1785 !state_debug.contains(secret),
1786 "ConfigState Debug leaked secret marker {secret}: {state_debug}"
1787 );
1788 }
1789 assert!(
1790 state_debug.contains("certificate_addresses_count: 1")
1791 && state_debug.contains("certificates_count: 1"),
1792 "ConfigState Debug must retain count-only certificate metadata: {state_debug}"
1793 );
1794 assert!(
1795 state_debug.len() <= 4096,
1796 "ConfigState Debug output is not bounded: {} bytes",
1797 state_debug.len()
1798 );
1799 }
1800
1801 #[test]
1802 fn certificate_fingerprint_control_debug_is_redacted_across_wrappers() {
1803 const FINGERPRINT_SECRET: &str = "CERTIFICATE_CONTROL_FINGERPRINT_SECRET_SENTINEL";
1804
1805 let fingerprint = format!("{FINGERPRINT_SECRET}{}", "f".repeat(4096));
1806 let remove = RemoveCertificate {
1807 address: Default::default(),
1808 fingerprint: fingerprint.clone(),
1809 };
1810 let replace = ReplaceCertificate {
1811 address: Default::default(),
1812 new_certificate: CertificateAndKey::default(),
1813 old_fingerprint: fingerprint.clone(),
1814 new_expired_at: None,
1815 };
1816 let remove_request = Request::from(RequestType::RemoveCertificate(remove.clone()));
1817 let replace_request = Request::from(RequestType::ReplaceCertificate(replace.clone()));
1818 let outputs = [
1819 ("RemoveCertificate", format!("{remove:?}")),
1820 ("ReplaceCertificate", format!("{replace:?}")),
1821 ("remove Request", format!("{remove_request:?}")),
1822 ("replace Request", format!("{replace_request:?}")),
1823 (
1824 "remove WorkerRequest",
1825 format!(
1826 "{:?}",
1827 WorkerRequest {
1828 id: "remove-certificate-test".to_owned(),
1829 content: remove_request,
1830 }
1831 ),
1832 ),
1833 (
1834 "replace WorkerRequest",
1835 format!(
1836 "{:?}",
1837 WorkerRequest {
1838 id: "replace-certificate-test".to_owned(),
1839 content: replace_request,
1840 }
1841 ),
1842 ),
1843 ];
1844
1845 for (label, output) in outputs {
1846 assert!(
1847 !output.contains(FINGERPRINT_SECRET),
1848 "{label} Debug leaked the certificate fingerprint: {output}"
1849 );
1850 assert!(
1851 output.contains(&format!("fingerprint_len: {}", fingerprint.len())),
1852 "{label} Debug omitted bounded fingerprint metadata: {output}"
1853 );
1854 assert!(
1855 output.len() <= 1024,
1856 "{label} Debug output is not bounded: {} bytes",
1857 output.len()
1858 );
1859 }
1860 }
1861
1862 #[test]
1863 fn cluster_debug_redacts_answers_and_authorization_data_across_wrappers() {
1864 const CLUSTER_SECRET: &str = "CLUSTER_ID_SECRET_SENTINEL";
1865 const LEGACY_ANSWER_SECRET: &str = "CLUSTER_LEGACY_ANSWER_SECRET_SENTINEL";
1866 const ANSWER_KEY_SECRET: &str = "CLUSTER_ANSWER_KEY_SECRET_SENTINEL";
1867 const ANSWER_BODY_SECRET: &str = "CLUSTER_ANSWER_BODY_SECRET_SENTINEL";
1868 const AUTH_HASH_SECRET: &str = "CLUSTER_AUTH_HASH_SECRET_SENTINEL";
1869 const REALM_SECRET: &str = "CLUSTER_AUTH_REALM_SECRET_SENTINEL";
1870
1871 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1872 let cluster = Cluster {
1873 cluster_id: long_value(CLUSTER_SECRET),
1874 answer_503: Some(long_value(LEGACY_ANSWER_SECRET)),
1875 answers: BTreeMap::from([(
1876 long_value(ANSWER_KEY_SECRET),
1877 long_value(ANSWER_BODY_SECRET),
1878 )]),
1879 authorized_hashes: vec![long_value(AUTH_HASH_SECRET)],
1880 www_authenticate: Some(long_value(REALM_SECRET)),
1881 ..Default::default()
1882 };
1883 let request = Request::from(RequestType::AddCluster(cluster.clone()));
1884 let outputs = [
1885 ("Cluster", format!("{cluster:?}")),
1886 ("Request", format!("{request:?}")),
1887 (
1888 "WorkerRequest",
1889 format!(
1890 "{:?}",
1891 WorkerRequest {
1892 id: "cluster-redaction-test".to_owned(),
1893 content: request,
1894 }
1895 ),
1896 ),
1897 ];
1898
1899 for (label, output) in outputs {
1900 for secret in [
1901 CLUSTER_SECRET,
1902 LEGACY_ANSWER_SECRET,
1903 ANSWER_KEY_SECRET,
1904 ANSWER_BODY_SECRET,
1905 AUTH_HASH_SECRET,
1906 REALM_SECRET,
1907 ] {
1908 assert!(
1909 !output.contains(secret),
1910 "{label} Debug leaked cluster marker {secret}: {output}"
1911 );
1912 }
1913 for metadata in [
1914 "cluster_id_len:",
1915 "answers_count: 1",
1916 "authorized_hashes_count: 1",
1917 ] {
1918 assert!(
1919 output.contains(metadata),
1920 "{label} Debug omitted bounded cluster metadata {metadata}: {output}"
1921 );
1922 }
1923 assert!(
1924 output.len() <= 2048,
1925 "{label} Debug output is not bounded: {} bytes",
1926 output.len()
1927 );
1928 }
1929 }
1930
1931 #[test]
1932 fn tcp_frontend_debug_redacts_stored_sni_routing_data_across_wrappers() {
1933 const CLUSTER_SECRET: &str = "TCP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1934 const TAG_KEY_SECRET: &str = "TCP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1935 const TAG_VALUE_SECRET: &str = "TCP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1936 const SNI_SECRET: &str = "TCP_FRONTEND_SNI_SECRET_SENTINEL";
1937 const ALPN_SECRET: &str = "TCP_FRONTEND_ALPN_SECRET_SENTINEL";
1938
1939 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1940 let frontend = RequestTcpFrontend {
1941 cluster_id: long_value(CLUSTER_SECRET),
1942 address: Default::default(),
1943 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1944 sni: Some(long_value(SNI_SECRET)),
1945 alpn: vec![long_value(ALPN_SECRET)],
1946 };
1947 let request = Request::from(RequestType::AddTcpFrontend(frontend.clone()));
1948 let outputs = [
1949 ("RequestTcpFrontend", format!("{frontend:?}")),
1950 ("Request", format!("{request:?}")),
1951 (
1952 "WorkerRequest",
1953 format!(
1954 "{:?}",
1955 WorkerRequest {
1956 id: "tcp-frontend-redaction-test".to_owned(),
1957 content: request,
1958 }
1959 ),
1960 ),
1961 ];
1962
1963 for (label, output) in outputs {
1964 for secret in [
1965 CLUSTER_SECRET,
1966 TAG_KEY_SECRET,
1967 TAG_VALUE_SECRET,
1968 SNI_SECRET,
1969 ALPN_SECRET,
1970 ] {
1971 assert!(
1972 !output.contains(secret),
1973 "{label} Debug leaked TCP frontend marker {secret}: {output}"
1974 );
1975 }
1976 for metadata in [
1977 "cluster_id_len:",
1978 "tags_count: 1",
1979 "sni_len: Some(",
1980 "alpn_count: 1",
1981 ] {
1982 assert!(
1983 output.contains(metadata),
1984 "{label} Debug omitted bounded TCP frontend metadata {metadata}: {output}"
1985 );
1986 }
1987 assert!(
1988 output.len() <= 1024,
1989 "{label} Debug output is not bounded: {} bytes",
1990 output.len()
1991 );
1992 }
1993 }
1994
1995 #[test]
1996 fn merge_relocates_single_worker_to_top_level() {
1997 let mut worker = WorkerMetrics {
2003 proxy: BTreeMap::new(),
2004 clusters: BTreeMap::new(),
2005 };
2006 worker.proxy.insert(
2007 "requests".to_owned(),
2008 FilteredMetrics {
2009 inner: Some(Inner::Count(42)),
2010 },
2011 );
2012 let mut cluster = ClusterMetrics {
2013 cluster: BTreeMap::new(),
2014 backends: Vec::new(),
2015 };
2016 cluster.cluster.insert(
2017 "requests".to_owned(),
2018 FilteredMetrics {
2019 inner: Some(Inner::Count(7)),
2020 },
2021 );
2022 worker.clusters.insert("cluster-a".to_owned(), cluster);
2023
2024 let mut agg = AggregatedMetrics {
2025 main: BTreeMap::new(),
2026 workers: BTreeMap::from([("0".to_owned(), worker)]),
2027 clusters: BTreeMap::new(),
2028 proxying: BTreeMap::new(),
2029 };
2030
2031 agg.merge_metrics();
2032
2033 assert!(
2034 agg.workers.is_empty(),
2035 "merge takes ownership of the per-worker map"
2036 );
2037 assert_eq!(
2038 agg.proxying.get("requests"),
2039 Some(&FilteredMetrics {
2040 inner: Some(Inner::Count(42)),
2041 }),
2042 "single worker's proxy counter must surface in proxying"
2043 );
2044 let cluster_a = agg
2045 .clusters
2046 .get("cluster-a")
2047 .expect("cluster row must surface in top-level clusters");
2048 assert_eq!(
2049 cluster_a.cluster.get("requests"),
2050 Some(&FilteredMetrics {
2051 inner: Some(Inner::Count(7)),
2052 })
2053 );
2054 }
2055
2056 #[test]
2057 fn merge_counts_and_gauges() {
2058 let mut gauge_a = FilteredMetrics {
2059 inner: Some(Inner::Gauge(4)),
2060 };
2061 let gauge_b = FilteredMetrics {
2062 inner: Some(Inner::Gauge(4)),
2063 };
2064
2065 gauge_a.merge(&gauge_b);
2066
2067 assert_eq!(
2068 gauge_a,
2069 FilteredMetrics {
2070 inner: Some(Inner::Gauge(8)),
2071 }
2072 );
2073
2074 let mut count_a = FilteredMetrics {
2075 inner: Some(Inner::Count(3)),
2076 };
2077 let count_b = FilteredMetrics {
2078 inner: Some(Inner::Count(3)),
2079 };
2080
2081 count_a.merge(&count_b);
2082
2083 assert_eq!(
2084 count_a,
2085 FilteredMetrics {
2086 inner: Some(Inner::Count(6)),
2087 }
2088 );
2089 }
2090
2091 #[test]
2092 fn merge_percentiles_takes_max_per_quantile() {
2093 let mut left = FilteredMetrics {
2097 inner: Some(Inner::Percentiles(Percentiles {
2098 samples: 100,
2099 p_50: 5,
2100 p_90: 20,
2101 p_99: 100,
2102 p_99_9: 200,
2103 p_99_99: 250,
2104 p_99_999: 300,
2105 p_100: 400,
2106 sum: 12_000,
2107 })),
2108 };
2109 let right = FilteredMetrics {
2110 inner: Some(Inner::Percentiles(Percentiles {
2111 samples: 50,
2112 p_50: 7,
2113 p_90: 15,
2114 p_99: 80,
2115 p_99_9: 240,
2116 p_99_99: 245,
2117 p_99_999: 290,
2118 p_100: 380,
2119 sum: 6_000,
2120 })),
2121 };
2122 left.merge(&right);
2123 assert_eq!(
2124 left,
2125 FilteredMetrics {
2126 inner: Some(Inner::Percentiles(Percentiles {
2127 samples: 150,
2128 p_50: 7,
2129 p_90: 20,
2130 p_99: 100,
2131 p_99_9: 240,
2132 p_99_99: 250,
2133 p_99_999: 300,
2134 p_100: 400,
2135 sum: 18_000,
2136 })),
2137 }
2138 );
2139 }
2140
2141 #[test]
2142 fn merge_histograms() {
2143 let mut histogram_a = FilteredMetrics {
2144 inner: Some(Inner::Histogram(FilteredHistogram {
2145 sum: 95,
2146 count: 30,
2147 buckets: vec![
2148 Bucket { le: 0, count: 1 },
2149 Bucket { le: 1, count: 2 },
2150 Bucket { le: 3, count: 10 },
2151 Bucket { le: 7, count: 25 },
2152 Bucket { le: 15, count: 27 },
2153 Bucket { le: 31, count: 30 },
2154 ],
2155 })),
2156 };
2157
2158 let histogram_b = FilteredMetrics {
2159 inner: Some(Inner::Histogram(FilteredHistogram {
2160 sum: 82,
2161 count: 40,
2162 buckets: vec![
2163 Bucket { le: 0, count: 0 },
2164 Bucket { le: 1, count: 0 },
2165 Bucket { le: 3, count: 12 },
2166 Bucket { le: 7, count: 30 },
2167 Bucket { le: 15, count: 40 },
2168 ],
2170 })),
2171 };
2172
2173 histogram_a.merge(&histogram_b);
2174
2175 let merged_histogram = FilteredMetrics {
2176 inner: Some(Inner::Histogram(FilteredHistogram {
2177 sum: 177,
2178 count: 70,
2179 buckets: vec![
2180 Bucket { le: 0, count: 1 },
2181 Bucket { le: 1, count: 2 },
2182 Bucket { le: 3, count: 22 },
2183 Bucket { le: 7, count: 55 },
2184 Bucket { le: 15, count: 67 },
2185 Bucket { le: 31, count: 70 }, ],
2187 })),
2188 };
2189
2190 assert_eq!(histogram_a, merged_histogram);
2191 }
2192
2193 const HEALTH_CHECK_SECRET: &str = "SYNTHETIC-DEBUG-HC-SECRET-a91d";
2194 const HEALTH_CHECK_FINGERPRINT: &str = "sozu-hc-hmac-sha256:v1:00112233445566778899aabbccddeeff:0000000000000000000000000000000000000000000000000000000000000000";
2195
2196 fn fingerprinted_health_check() -> super::command::HealthCheckConfig {
2197 use super::command::{HealthCheckConfig, HealthCheckHeader};
2198 HealthCheckConfig {
2199 uri: "/".to_owned(),
2200 interval: 10,
2201 timeout: 5,
2202 healthy_threshold: 3,
2203 unhealthy_threshold: 3,
2204 headers: vec![HealthCheckHeader {
2205 name: "Authorization".to_owned(),
2206 fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2207 }],
2208 ..Default::default()
2209 }
2210 }
2211
2212 #[test]
2216 fn health_check_header_debug_shows_metadata_and_never_the_ingress_value() {
2217 use super::command::{
2218 HealthCheckHeader, HealthCheckHeaderValue, SetHealthCheck, SetProtectedHealthCheck,
2219 };
2220
2221 let long_name = format!("X-{}", "n".repeat(200));
2222 let alone = format!(
2223 "{:?}",
2224 HealthCheckHeader {
2225 name: long_name.clone(),
2226 fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2227 }
2228 );
2229 assert!(!alone.contains(&long_name), "the name is bounded: {alone}");
2230 assert!(alone.contains("name_truncated: true"), "{alone}");
2231 assert!(alone.contains(HEALTH_CHECK_FINGERPRINT), "{alone}");
2232 let wide = format!(
2234 "{:?}",
2235 HealthCheckHeader {
2236 name: "\u{e9}".repeat(100),
2237 fingerprint: String::new(),
2238 }
2239 );
2240 assert!(wide.contains("name_truncated: true"), "{wide}");
2241
2242 let config = fingerprinted_health_check();
2243 let carrier = HealthCheckHeaderValue {
2244 name: "Authorization".to_owned(),
2245 value: HEALTH_CHECK_SECRET.as_bytes().to_vec(),
2246 };
2247 let protected = Request::from(RequestType::SetProtectedHealthCheck(
2248 SetProtectedHealthCheck {
2249 cluster_id: "app".to_owned(),
2250 config: config.to_owned(),
2251 values: vec![carrier.to_owned()],
2252 },
2253 ));
2254 let worker_request = WorkerRequest::new("id".to_owned(), protected.to_owned());
2255 let legacy = Request::from(RequestType::SetHealthCheck(SetHealthCheck {
2256 cluster_id: "app".to_owned(),
2257 config,
2258 }));
2259 let numeric = format!("{:?}", HEALTH_CHECK_SECRET.as_bytes());
2260 for (what, rendered) in [
2261 ("carrier", format!("{carrier:?}")),
2262 ("carrier pretty", format!("{carrier:#?}")),
2263 ("protected request", format!("{protected:?}")),
2264 ("protected request pretty", format!("{protected:#?}")),
2265 ("worker request", format!("{worker_request:?}")),
2266 ("legacy request", format!("{legacy:?}")),
2267 ] {
2268 assert!(
2269 !rendered.contains(HEALTH_CHECK_SECRET) && !rendered.contains(&numeric),
2270 "{what} Debug leaked a header value: {rendered}"
2271 );
2272 if what.starts_with("carrier") {
2273 assert!(rendered.contains("<protected>"), "{what}: {rendered}");
2274 }
2275 }
2276 }
2277}