WEB Debug + Trace

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-25 12:59:41 +03:00
parent 0779cd0901
commit e774bc8c9a
48 changed files with 3744 additions and 435 deletions
+11
View File
@@ -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)
+16 -1
View File
@@ -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",
+8
View File
@@ -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,
+9 -32
View File
@@ -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(())
}
+31
View File
@@ -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(())
}
+69
View File
@@ -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(())
}