mirror of
https://github.com/telemt/telemt.git
synced 2026-09-16 15:34:13 +03:00
WEB Debug + Trace
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -36,7 +36,7 @@ use super::load::{LoadedConfig, ProxyConfig};
|
||||
#[allow(unused_imports)]
|
||||
use crate::config::{
|
||||
CidrRateLimitKey, LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel,
|
||||
MeWriterPickMode,
|
||||
MeWriterPickMode, WebDebugConfig, web_debug_fits_limits,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::config::{ListenerConfig, SynLimitMode};
|
||||
|
||||
@@ -89,6 +89,7 @@ pub struct HotFields {
|
||||
pub user_max_unique_ips_global_each: usize,
|
||||
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
||||
pub user_max_unique_ips_window_secs: u64,
|
||||
pub web_debug: WebDebugConfig,
|
||||
}
|
||||
|
||||
impl HotFields {
|
||||
@@ -218,6 +219,7 @@ impl HotFields {
|
||||
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
|
||||
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
||||
user_max_unique_ips_window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||
web_debug: cfg.web.debug.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,6 +342,9 @@ pub(super) fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyC
|
||||
let process_limits = cfg.web.limits.clone();
|
||||
cfg.web = new.web.clone();
|
||||
cfg.web.limits = process_limits;
|
||||
if !web_debug_fits_limits(&cfg.web.debug, &cfg.web.limits) {
|
||||
cfg.web.debug = old.web.debug.clone();
|
||||
}
|
||||
if cfg.rebuild_runtime_user_auth().is_err() {
|
||||
cfg.runtime_user_auth = None;
|
||||
}
|
||||
|
||||
@@ -496,4 +496,13 @@ pub(super) fn log_changes(
|
||||
new_hot.user_max_unique_ips_window_secs
|
||||
);
|
||||
}
|
||||
if old_hot.web_debug != new_hot.web_debug {
|
||||
info!(
|
||||
"config reload: web.debug updated: enabled={} body_capture={:?} window={}..={}s",
|
||||
new_hot.web_debug.enabled,
|
||||
new_hot.web_debug.body_capture,
|
||||
new_hot.web_debug.default_window_secs,
|
||||
new_hot.web_debug.max_window_secs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,42 @@ fn bind_stale_mode_is_hot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_debug_policy_is_hot_while_debug_capacity_is_process_owned() {
|
||||
let old = sample_config();
|
||||
let mut new = old.clone();
|
||||
new.web.debug.enabled = true;
|
||||
new.web.debug.default_window_secs = 60;
|
||||
new.web.limits.debug_records_capacity += 1;
|
||||
|
||||
let applied = overlay_hot_fields(&old, &new);
|
||||
assert!(applied.web.debug.enabled);
|
||||
assert_eq!(applied.web.debug.default_window_secs, 60);
|
||||
assert_eq!(
|
||||
applied.web.limits.debug_records_capacity,
|
||||
old.web.limits.debug_records_capacity
|
||||
);
|
||||
assert_ne!(
|
||||
HotFields::from_config(&old),
|
||||
HotFields::from_config(&applied)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_debug_prefix_requiring_deferred_capacity_is_not_hot_applied() {
|
||||
let old = sample_config();
|
||||
let mut new = old.clone();
|
||||
new.web.limits.max_body_bytes = 4 * 1024 * 1024;
|
||||
new.web.debug.body_prefix_bytes = 3 * 1024 * 1024;
|
||||
|
||||
let applied = overlay_hot_fields(&old, &new);
|
||||
assert_eq!(applied.web.limits.max_body_bytes, old.web.limits.max_body_bytes);
|
||||
assert_eq!(
|
||||
applied.web.debug.body_prefix_bytes,
|
||||
old.web.debug.body_prefix_bytes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keepalive_is_not_hot() {
|
||||
let old = sample_config();
|
||||
|
||||
@@ -14,6 +14,7 @@ use sha2::{Digest, Sha256};
|
||||
use super::*;
|
||||
|
||||
const WEB_CAPABILITY_CONTEXT: &[u8] = b"tdesktop-web-proxy-bridge-v1\n";
|
||||
const WEB_DEBUG_FINGERPRINT_CONTEXT: &[u8] = b"telemt-web-debug-key-fingerprint-v1\0";
|
||||
const MAX_WEB_STATIC_DEPTH: usize = 64;
|
||||
|
||||
/// Builds the immutable WEB routing and decoy snapshot for one generation.
|
||||
@@ -49,6 +50,8 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
client_secret(auth_entry.secret, profile.secret_mode);
|
||||
let capability =
|
||||
derive_web_capability(&client_secret[..client_secret_len], vhost.host.as_bytes())?;
|
||||
let key_fingerprint =
|
||||
debug_key_fingerprint(&client_secret[..client_secret_len]);
|
||||
if !capabilities.insert(capability) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB vhost `{}` contains profiles with the same client capability",
|
||||
@@ -62,6 +65,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
secret_mode: profile.secret_mode,
|
||||
carrier: config.web.carrier,
|
||||
capability,
|
||||
key_fingerprint,
|
||||
max_sessions: profile
|
||||
.max_sessions
|
||||
.unwrap_or(config.web.limits.max_sessions_global),
|
||||
@@ -93,6 +97,13 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn debug_key_fingerprint(secret: &[u8]) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(WEB_DEBUG_FINGERPRINT_CONTEXT);
|
||||
digest.update(secret);
|
||||
hex::encode(&digest.finalize()[..8])
|
||||
}
|
||||
|
||||
/// Derives the Telegram Desktop WEB capability for one exact secret and host.
|
||||
pub(crate) fn derive_web_capability(secret: &[u8], host: &[u8]) -> Result<[u8; 32]> {
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret)
|
||||
|
||||
@@ -259,7 +259,7 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[
|
||||
"web_trusted_proxy_cidrs",
|
||||
];
|
||||
|
||||
const WEB_CONFIG_KEYS: &[&str] = &["enabled", "carrier", "limits", "timeouts", "vhosts"];
|
||||
const WEB_CONFIG_KEYS: &[&str] = &["enabled", "carrier", "debug", "limits", "timeouts", "vhosts"];
|
||||
|
||||
const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_header_bytes",
|
||||
@@ -290,6 +290,8 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_static_files",
|
||||
"max_static_file_bytes",
|
||||
"max_static_bytes",
|
||||
"debug_records_capacity",
|
||||
"debug_bytes_global",
|
||||
"memory_envelope_bytes",
|
||||
"new_bootstraps_per_minute",
|
||||
"new_bootstraps_burst",
|
||||
@@ -299,6 +301,19 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"new_streams_burst",
|
||||
];
|
||||
|
||||
const WEB_DEBUG_CONFIG_KEYS: &[&str] = &[
|
||||
"enabled",
|
||||
"capture_lifecycle",
|
||||
"capture_headers",
|
||||
"capture_timings",
|
||||
"capture_frames",
|
||||
"body_capture",
|
||||
"body_prefix_bytes",
|
||||
"decoy_body_prefix_bytes",
|
||||
"default_window_secs",
|
||||
"max_window_secs",
|
||||
];
|
||||
|
||||
const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"header_secs",
|
||||
"body_secs",
|
||||
|
||||
@@ -37,6 +37,7 @@ fn known_config_keys_for_suggestion() -> Vec<&'static str> {
|
||||
LISTENER_CONFIG_KEYS,
|
||||
WEB_CONFIG_KEYS,
|
||||
WEB_LIMITS_CONFIG_KEYS,
|
||||
WEB_DEBUG_CONFIG_KEYS,
|
||||
WEB_TIMEOUTS_CONFIG_KEYS,
|
||||
WEB_VHOST_CONFIG_KEYS,
|
||||
WEB_DECOY_CONFIG_KEYS,
|
||||
@@ -241,6 +242,13 @@ pub(super) fn collect_unknown_config_keys(parsed_toml: &toml::Value) -> Vec<Unkn
|
||||
&["web", "limits"],
|
||||
WEB_LIMITS_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
&known_for_suggestion,
|
||||
&["web", "debug"],
|
||||
WEB_DEBUG_CONFIG_KEYS,
|
||||
);
|
||||
check_known_table(
|
||||
parsed_toml,
|
||||
&mut unknown,
|
||||
|
||||
@@ -2,6 +2,11 @@ use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Debug capture validation is independent from restart-only storage limits.
|
||||
mod debug;
|
||||
// Memory-envelope arithmetic remains isolated from protocol validation.
|
||||
mod memory;
|
||||
|
||||
const WEB_FRAME_HEADER_BYTES: usize = 8;
|
||||
const WEB_QUEUE_ITEM_COST: usize = 256;
|
||||
const WEB_CONTROL_EXTRA_ITEMS: usize = 16;
|
||||
@@ -59,6 +64,7 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> {
|
||||
}
|
||||
|
||||
validate_limits(&config.web.limits)?;
|
||||
debug::validate(&config.web.debug, &config.web.limits)?;
|
||||
if config.web.carrier == WebCarrier::HttpsLanes && config.web.limits.max_http_handlers < 2 {
|
||||
return config_error("web.carrier=https-lanes requires web.limits.max_http_handlers >= 2");
|
||||
}
|
||||
@@ -175,6 +181,8 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
|
||||
("max_static_files", limits.max_static_files),
|
||||
("max_static_file_bytes", limits.max_static_file_bytes),
|
||||
("max_static_bytes", limits.max_static_bytes),
|
||||
("debug_records_capacity", limits.debug_records_capacity),
|
||||
("debug_bytes_global", limits.debug_bytes_global),
|
||||
("memory_envelope_bytes", limits.memory_envelope_bytes),
|
||||
];
|
||||
if let Some((field, _)) = positive.into_iter().find(|(_, value)| *value == 0) {
|
||||
@@ -309,38 +317,7 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
|
||||
"web.limits pending ceilings must preserve one uplink batch and downlink progress",
|
||||
);
|
||||
}
|
||||
let body_reservation = limits
|
||||
.max_body_readers
|
||||
.checked_mul(limits.max_body_bytes)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.limits body reader reservation overflowed usize".to_string())
|
||||
})?;
|
||||
if body_reservation > limits.max_body_bytes_global
|
||||
|| limits.max_body_bytes_global > u32::MAX as usize
|
||||
{
|
||||
return config_error(
|
||||
"web.limits max_body_readers * max_body_bytes must fit max_body_bytes_global and u32",
|
||||
);
|
||||
}
|
||||
let http_header_reservation = limits
|
||||
.max_http_connections
|
||||
.checked_mul(limits.max_header_bytes)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.limits HTTP header reservations overflow usize".to_string())
|
||||
})?;
|
||||
let reserved = limits
|
||||
.pending_bytes_global
|
||||
.checked_add(limits.max_body_bytes_global)
|
||||
.and_then(|value| value.checked_add(limits.max_static_bytes))
|
||||
.and_then(|value| value.checked_add(http_header_reservation))
|
||||
.ok_or_else(|| ProxyError::Config("web.limits byte ceilings overflow usize".to_string()))?;
|
||||
if reserved > limits.memory_envelope_bytes
|
||||
|| limits.memory_envelope_bytes > MAX_WEB_MEMORY_ENVELOPE_BYTES
|
||||
{
|
||||
return config_error(
|
||||
"web.limits memory reservations must fit memory_envelope_bytes within 4 GiB",
|
||||
);
|
||||
}
|
||||
memory::validate(limits)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
|
||||
const MAX_WEB_TRACE_WINDOW_SECS: u64 = 86_400;
|
||||
const MIN_WEB_DEBUG_BYTES_GLOBAL: usize = 4096;
|
||||
|
||||
/// Validates hot debug policy independently from process-owned storage limits.
|
||||
pub(super) fn validate(policy: &WebDebugConfig, limits: &WebLimitsConfig) -> Result<()> {
|
||||
if limits.debug_bytes_global < MIN_WEB_DEBUG_BYTES_GLOBAL {
|
||||
return config_error("web.limits.debug_bytes_global must be at least 4096 bytes");
|
||||
}
|
||||
if policy.default_window_secs == 0
|
||||
|| policy.max_window_secs == 0
|
||||
|| policy.default_window_secs > policy.max_window_secs
|
||||
|| policy.max_window_secs > MAX_WEB_TRACE_WINDOW_SECS
|
||||
{
|
||||
return config_error(
|
||||
"web.debug windows must be non-zero, ordered, and no greater than 86400 seconds",
|
||||
);
|
||||
}
|
||||
if policy.body_prefix_bytes > limits.max_body_bytes {
|
||||
return config_error("web.debug.body_prefix_bytes must not exceed web.limits.max_body_bytes");
|
||||
}
|
||||
if policy.body_prefix_bytes > limits.debug_bytes_global
|
||||
|| policy.decoy_body_prefix_bytes > limits.debug_bytes_global
|
||||
{
|
||||
return config_error(
|
||||
"web.debug body prefixes must not exceed web.limits.debug_bytes_global",
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use super::*;
|
||||
|
||||
const WEB_DEBUG_RENDERERS: usize = 2;
|
||||
const WEB_DEBUG_STATUS_PAGE_BYTES: usize = 8 * 1024 * 1024;
|
||||
const WEB_DEBUG_GROUP_SCRATCH_BYTES: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Validates process-wide body, header, queue, static, and debug reservations.
|
||||
pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
let body_reservation = limits
|
||||
.max_body_readers
|
||||
.checked_mul(limits.max_body_bytes)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.limits body reader reservation overflowed usize".to_string())
|
||||
})?;
|
||||
if body_reservation > limits.max_body_bytes_global
|
||||
|| limits.max_body_bytes_global > u32::MAX as usize
|
||||
{
|
||||
return config_error(
|
||||
"web.limits max_body_readers * max_body_bytes must fit max_body_bytes_global and u32",
|
||||
);
|
||||
}
|
||||
let http_header_reservation = limits
|
||||
.max_http_connections
|
||||
.checked_mul(limits.max_header_bytes)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.limits HTTP header reservations overflow usize".to_string())
|
||||
})?;
|
||||
let debug_ring_index = limits
|
||||
.debug_records_capacity
|
||||
.checked_mul(std::mem::size_of::<usize>())
|
||||
.ok_or_else(|| ProxyError::Config("web.limits debug index overflowed usize".to_string()))?;
|
||||
let status_pages = WEB_DEBUG_RENDERERS
|
||||
.checked_mul(WEB_DEBUG_STATUS_PAGE_BYTES)
|
||||
.ok_or_else(|| ProxyError::Config("web.debug status pages overflowed usize".to_string()))?;
|
||||
let debug_reservation = limits
|
||||
.debug_bytes_global
|
||||
.checked_add(
|
||||
debug_ring_index
|
||||
.checked_mul(WEB_DEBUG_RENDERERS)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.debug snapshot indexes overflowed usize".to_string())
|
||||
})?,
|
||||
)
|
||||
.and_then(|value| {
|
||||
WEB_DEBUG_RENDERERS
|
||||
.checked_mul(WEB_DEBUG_GROUP_SCRATCH_BYTES)
|
||||
.and_then(|scratch| value.checked_add(scratch))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.debug reservations overflowed usize".to_string())
|
||||
})?;
|
||||
let reserved = limits
|
||||
.pending_bytes_global
|
||||
.checked_add(limits.max_body_bytes_global)
|
||||
.and_then(|value| value.checked_add(limits.max_static_bytes))
|
||||
.and_then(|value| value.checked_add(debug_ring_index))
|
||||
.and_then(|value| value.checked_add(status_pages))
|
||||
.and_then(|value| value.checked_add(debug_reservation))
|
||||
.and_then(|value| value.checked_add(http_header_reservation))
|
||||
.ok_or_else(|| ProxyError::Config("web.limits byte ceilings overflow usize".to_string()))?;
|
||||
if reserved > limits.memory_envelope_bytes
|
||||
|| limits.memory_envelope_bytes > MAX_WEB_MEMORY_ENVELOPE_BYTES
|
||||
{
|
||||
return config_error(
|
||||
"web.limits memory reservations must fit memory_envelope_bytes within 4 GiB",
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -47,6 +47,53 @@ fn web_config_builds_canonical_runtime_snapshot() {
|
||||
assert_eq!(vhost.profiles[0].max_sessions, 4);
|
||||
assert_eq!(vhost.profiles[0].max_streams, 64);
|
||||
assert_eq!(vhost.profiles[0].max_streams_per_session, 16);
|
||||
assert_eq!(vhost.profiles[0].key_fingerprint.len(), 16);
|
||||
assert_ne!(vhost.profiles[0].key_fingerprint, "0001020304050607");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_debug_table_uses_debug_name_and_bounded_defaults() {
|
||||
let configured = WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.debug]\nenabled = true\nbody_capture = \"prefix\"\nbody_prefix_bytes = 2048\ndefault_window_secs = 180\nmax_window_secs = 900\n\n[[web.vhosts]]",
|
||||
);
|
||||
let config = load_config_from_temp_toml(&configured);
|
||||
assert!(config.web.debug.enabled);
|
||||
assert_eq!(config.web.debug.body_capture, WebDebugBodyCapture::Prefix);
|
||||
assert_eq!(config.web.debug.body_prefix_bytes, 2048);
|
||||
assert_eq!(config.web.debug.default_window_secs, 180);
|
||||
assert_eq!(config.web.debug.max_window_secs, 900);
|
||||
|
||||
let old_name = format!("[general]\nconfig_strict = true\n{}", WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.trace]\nenabled = true\n\n[[web.vhosts]]",
|
||||
));
|
||||
let error = load_config_error_from_temp_toml(&old_name);
|
||||
assert!(error.contains("web.trace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_debug_prefix_and_window_validation_fail_closed() {
|
||||
let oversized_prefix = WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.debug]\nenabled = true\nbody_prefix_bytes = 2097153\n\n[[web.vhosts]]",
|
||||
);
|
||||
let error = load_config_error_from_temp_toml(&oversized_prefix);
|
||||
assert!(error.contains("web.debug.body_prefix_bytes"));
|
||||
|
||||
let reversed_window = WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.debug]\nenabled = true\ndefault_window_secs = 181\nmax_window_secs = 180\n\n[[web.vhosts]]",
|
||||
);
|
||||
let error = load_config_error_from_temp_toml(&reversed_window);
|
||||
assert!(error.contains("web.debug windows"));
|
||||
|
||||
let undersized_store = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"https-lanes\"\n\n[web.limits]\ndebug_bytes_global = 4095",
|
||||
);
|
||||
let error = load_config_error_from_temp_toml(&undersized_store);
|
||||
assert!(error.contains("debug_bytes_global must be at least 4096"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,6 +24,8 @@ mod network;
|
||||
mod policies;
|
||||
mod server;
|
||||
mod web;
|
||||
// WEB debug capture policy is reusable by config reload and process storage.
|
||||
mod web_debug;
|
||||
|
||||
pub use access::{AccessConfig, CidrRateLimitKey, RateLimitBps};
|
||||
#[allow(unused_imports)]
|
||||
@@ -52,6 +54,8 @@ pub use web::{
|
||||
WebCarrier, WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode,
|
||||
WebTimeoutsConfig, WebVhostConfig,
|
||||
};
|
||||
pub use web_debug::{WebDebugBodyCapture, WebDebugConfig};
|
||||
pub(crate) use web_debug::web_debug_fits_limits;
|
||||
pub(crate) use web::{
|
||||
WebRuntimeConfig, WebRuntimeDecoy, WebRuntimeProfile, WebRuntimeVhost, WebStaticAsset,
|
||||
WebStaticSite,
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::sync::Arc;
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::web_debug::WebDebugConfig;
|
||||
|
||||
/// Client-facing secret representation used to derive a WEB capability.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -175,6 +177,12 @@ pub struct WebLimitsConfig {
|
||||
/// Maximum static snapshot bytes across all virtual hosts.
|
||||
#[serde(default = "default_web_max_static_bytes")]
|
||||
pub max_static_bytes: usize,
|
||||
/// Maximum retained WEB debug record count.
|
||||
#[serde(default = "default_web_debug_records_capacity")]
|
||||
pub debug_records_capacity: usize,
|
||||
/// Process-wide retained and in-flight WEB debug byte ceiling.
|
||||
#[serde(default = "default_web_debug_bytes_global")]
|
||||
pub debug_bytes_global: usize,
|
||||
/// Declared process envelope for HTTP heads, bodies, queues, and static snapshots.
|
||||
#[serde(default = "default_web_memory_envelope_bytes")]
|
||||
pub memory_envelope_bytes: usize,
|
||||
@@ -229,6 +237,8 @@ impl Default for WebLimitsConfig {
|
||||
max_static_files: default_web_max_static_files(),
|
||||
max_static_file_bytes: default_web_max_static_file_bytes(),
|
||||
max_static_bytes: default_web_max_static_bytes(),
|
||||
debug_records_capacity: default_web_debug_records_capacity(),
|
||||
debug_bytes_global: default_web_debug_bytes_global(),
|
||||
memory_envelope_bytes: default_web_memory_envelope_bytes(),
|
||||
new_bootstraps_per_minute: default_web_new_bootstraps_per_minute(),
|
||||
new_bootstraps_burst: default_web_new_bootstraps_burst(),
|
||||
@@ -300,6 +310,9 @@ pub struct WebConfig {
|
||||
/// Hard process and protocol limits.
|
||||
#[serde(default)]
|
||||
pub limits: WebLimitsConfig,
|
||||
/// Hot-reloadable bounded server-side debug policy.
|
||||
#[serde(default)]
|
||||
pub debug: WebDebugConfig,
|
||||
/// WEB lifecycle deadlines.
|
||||
#[serde(default)]
|
||||
pub timeouts: WebTimeoutsConfig,
|
||||
@@ -348,6 +361,8 @@ pub(crate) struct WebRuntimeProfile {
|
||||
pub(crate) carrier: WebCarrier,
|
||||
/// HMAC-derived bridge capability.
|
||||
pub(crate) capability: [u8; 32],
|
||||
/// Non-secret domain-separated client-secret fingerprint for debugging.
|
||||
pub(crate) key_fingerprint: String,
|
||||
/// Per-profile live session ceiling.
|
||||
pub(crate) max_sessions: usize,
|
||||
/// Per-profile live logical-stream ceiling.
|
||||
@@ -439,6 +454,8 @@ usize_default!(default_web_max_profiles, 32);
|
||||
usize_default!(default_web_max_static_files, 4096);
|
||||
usize_default!(default_web_max_static_file_bytes, 8 * 1024 * 1024);
|
||||
usize_default!(default_web_max_static_bytes, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_debug_records_capacity, 65_536);
|
||||
usize_default!(default_web_debug_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_memory_envelope_bytes, 768 * 1024 * 1024);
|
||||
u32_default!(default_web_new_bootstraps_per_minute, 1200);
|
||||
u32_default!(default_web_new_bootstraps_burst, 256);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::web::WebLimitsConfig;
|
||||
|
||||
/// Request and response body retention policy for WEB debugging.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WebDebugBodyCapture {
|
||||
/// Omits body snapshots entirely.
|
||||
Off,
|
||||
/// Retains body lengths and completion state without payload bytes.
|
||||
#[default]
|
||||
Metadata,
|
||||
/// Retains a bounded prefix of each body.
|
||||
Prefix,
|
||||
/// Retains complete bounded carrier bodies and bounded decoy prefixes.
|
||||
Full,
|
||||
}
|
||||
|
||||
/// Hot-reloadable WEB server-side debugging policy.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WebDebugConfig {
|
||||
/// Enables process-owned WEB debug collection.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Records typed bridge, session, stream, handshake, and relay events.
|
||||
#[serde(default = "default_true")]
|
||||
pub capture_lifecycle: bool,
|
||||
/// Retains allowlisted header values and names of all other headers.
|
||||
#[serde(default = "default_true")]
|
||||
pub capture_headers: bool,
|
||||
/// Retains request service and body-consumption timing points.
|
||||
#[serde(default = "default_true")]
|
||||
pub capture_timings: bool,
|
||||
/// Parses carrier bodies into bounded frame metadata.
|
||||
#[serde(default = "default_true")]
|
||||
pub capture_frames: bool,
|
||||
/// Controls request and response body byte retention.
|
||||
#[serde(default)]
|
||||
pub body_capture: WebDebugBodyCapture,
|
||||
/// Maximum retained body prefix for recognized WEB requests.
|
||||
#[serde(default = "default_body_prefix_bytes")]
|
||||
pub body_prefix_bytes: usize,
|
||||
/// Maximum retained body prefix for ordinary decoy traffic.
|
||||
#[serde(default = "default_decoy_body_prefix_bytes")]
|
||||
pub decoy_body_prefix_bytes: usize,
|
||||
/// Default observation window presented by the status page.
|
||||
#[serde(default = "default_window_secs")]
|
||||
pub default_window_secs: u64,
|
||||
/// Largest observation window accepted by the status page.
|
||||
#[serde(default = "default_max_window_secs")]
|
||||
pub max_window_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for WebDebugConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
capture_lifecycle: true,
|
||||
capture_headers: true,
|
||||
capture_timings: true,
|
||||
capture_frames: true,
|
||||
body_capture: WebDebugBodyCapture::Metadata,
|
||||
body_prefix_bytes: default_body_prefix_bytes(),
|
||||
decoy_body_prefix_bytes: default_decoy_body_prefix_bytes(),
|
||||
default_window_secs: default_window_secs(),
|
||||
max_window_secs: default_max_window_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_body_prefix_bytes() -> usize {
|
||||
4096
|
||||
}
|
||||
|
||||
fn default_decoy_body_prefix_bytes() -> usize {
|
||||
4096
|
||||
}
|
||||
|
||||
fn default_window_secs() -> u64 {
|
||||
180
|
||||
}
|
||||
|
||||
fn default_max_window_secs() -> u64 {
|
||||
3600
|
||||
}
|
||||
|
||||
/// Checks whether a hot debug policy fits restart-frozen process capacities.
|
||||
pub(crate) fn web_debug_fits_limits(
|
||||
policy: &WebDebugConfig,
|
||||
limits: &WebLimitsConfig,
|
||||
) -> bool {
|
||||
policy.body_prefix_bytes <= limits.max_body_bytes
|
||||
&& policy.body_prefix_bytes <= limits.debug_bytes_global
|
||||
&& policy.decoy_body_prefix_bytes <= limits.debug_bytes_global
|
||||
}
|
||||
Reference in New Issue
Block a user