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