mirror of
https://github.com/telemt/telemt.git
synced 2026-10-29 03:57:15 +03:00
WEB: websocket + websocket-lanes as Carrier
This commit is contained in:
@@ -50,8 +50,7 @@ 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]);
|
||||
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",
|
||||
|
||||
@@ -259,7 +259,9 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[
|
||||
"web_trusted_proxy_cidrs",
|
||||
];
|
||||
|
||||
const WEB_CONFIG_KEYS: &[&str] = &["enabled", "carrier", "debug", "limits", "timeouts", "vhosts"];
|
||||
const WEB_CONFIG_KEYS: &[&str] = &[
|
||||
"enabled", "carrier", "debug", "limits", "timeouts", "vhosts",
|
||||
];
|
||||
|
||||
const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_header_bytes",
|
||||
@@ -269,6 +271,10 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_frames_per_body",
|
||||
"max_http_connections",
|
||||
"max_http_handlers",
|
||||
"websocket_bytes_global",
|
||||
"websocket_admission_watermark_pct",
|
||||
"websocket_eviction_watermark_pct",
|
||||
"websocket_http_connection_reserve",
|
||||
"max_body_readers",
|
||||
"max_body_bytes_global",
|
||||
"max_sessions_global",
|
||||
@@ -319,6 +325,9 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"body_secs",
|
||||
"stream_handshake_secs",
|
||||
"long_poll_secs",
|
||||
"websocket_write_secs",
|
||||
"websocket_backpressure_secs",
|
||||
"websocket_eviction_secs",
|
||||
"bootstrap_lifetime_secs",
|
||||
"reconnect_grace_secs",
|
||||
"http_idle_secs",
|
||||
|
||||
@@ -6,6 +6,8 @@ use super::*;
|
||||
mod debug;
|
||||
// Memory-envelope arithmetic remains isolated from protocol validation.
|
||||
mod memory;
|
||||
// WebSocket transport policy is validated independently from HTTP body policy.
|
||||
mod websocket;
|
||||
|
||||
const WEB_FRAME_HEADER_BYTES: usize = 8;
|
||||
const WEB_QUEUE_ITEM_COST: usize = 256;
|
||||
@@ -69,6 +71,7 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> {
|
||||
return config_error("web.carrier=https-lanes requires web.limits.max_http_handlers >= 2");
|
||||
}
|
||||
validate_timeouts(&config.web.timeouts)?;
|
||||
websocket::validate(config.web.carrier, &config.web.limits, &config.web.timeouts)?;
|
||||
validate_vhosts(config)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -327,6 +330,12 @@ fn validate_timeouts(timeouts: &WebTimeoutsConfig) -> Result<()> {
|
||||
("body_secs", timeouts.body_secs),
|
||||
("stream_handshake_secs", timeouts.stream_handshake_secs),
|
||||
("long_poll_secs", timeouts.long_poll_secs),
|
||||
("websocket_write_secs", timeouts.websocket_write_secs),
|
||||
(
|
||||
"websocket_backpressure_secs",
|
||||
timeouts.websocket_backpressure_secs,
|
||||
),
|
||||
("websocket_eviction_secs", timeouts.websocket_eviction_secs),
|
||||
("bootstrap_lifetime_secs", timeouts.bootstrap_lifetime_secs),
|
||||
("reconnect_grace_secs", timeouts.reconnect_grace_secs),
|
||||
("http_idle_secs", timeouts.http_idle_secs),
|
||||
|
||||
@@ -18,7 +18,9 @@ pub(super) fn validate(policy: &WebDebugConfig, limits: &WebLimitsConfig) -> Res
|
||||
);
|
||||
}
|
||||
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");
|
||||
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
|
||||
|
||||
@@ -46,9 +46,7 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
.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())
|
||||
})?;
|
||||
.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)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::*;
|
||||
|
||||
const MAX_WEBSOCKET_BATCH_BYTES: usize = 2 * 1024 * 1024;
|
||||
const WEBSOCKET_IO_BUFFER_BYTES: usize = 64 * 1024;
|
||||
const WEBSOCKET_DRIVER_OVERHEAD_BYTES: usize = 4 * 1024;
|
||||
const WEBSOCKET_FRAME_OVERHEAD_BYTES: usize = 14;
|
||||
|
||||
/// Validates WebSocket admission, memory, and deadline invariants.
|
||||
pub(super) fn validate(
|
||||
carrier: WebCarrier,
|
||||
limits: &WebLimitsConfig,
|
||||
timeouts: &WebTimeoutsConfig,
|
||||
) -> Result<()> {
|
||||
if !(1..100).contains(&limits.websocket_admission_watermark_pct)
|
||||
|| !(1..100).contains(&limits.websocket_eviction_watermark_pct)
|
||||
|| limits.websocket_admission_watermark_pct >= limits.websocket_eviction_watermark_pct
|
||||
{
|
||||
return config_error(
|
||||
"web.limits WebSocket watermarks must satisfy 1 <= admission < eviction < 100",
|
||||
);
|
||||
}
|
||||
if limits.websocket_bytes_global == 0 {
|
||||
return config_error("web.limits.websocket_bytes_global must be > 0");
|
||||
}
|
||||
if timeouts.websocket_eviction_secs > timeouts.websocket_write_secs {
|
||||
return config_error(
|
||||
"web.timeouts.websocket_eviction_secs must not exceed websocket_write_secs",
|
||||
);
|
||||
}
|
||||
if !carrier.uses_websocket() {
|
||||
return Ok(());
|
||||
}
|
||||
if limits.carrier_batch_bytes > MAX_WEBSOCKET_BATCH_BYTES {
|
||||
return config_error(
|
||||
"WebSocket carriers require web.limits.carrier_batch_bytes <= 2097152",
|
||||
);
|
||||
}
|
||||
if limits.websocket_http_connection_reserve == 0
|
||||
|| limits.websocket_http_connection_reserve >= limits.max_http_connections
|
||||
{
|
||||
return config_error(
|
||||
"WebSocket carriers require websocket_http_connection_reserve within [1, max_http_connections)",
|
||||
);
|
||||
}
|
||||
let socket_base = WEBSOCKET_IO_BUFFER_BYTES
|
||||
.checked_mul(2)
|
||||
.and_then(|value| value.checked_add(WEBSOCKET_DRIVER_OVERHEAD_BYTES))
|
||||
.ok_or_else(|| ProxyError::Config("WebSocket base reservation overflowed usize".into()))?;
|
||||
let minimum_websocket_progress = limits
|
||||
.carrier_batch_bytes
|
||||
.checked_add(WEBSOCKET_FRAME_OVERHEAD_BYTES)
|
||||
.and_then(|value| value.checked_mul(2))
|
||||
.and_then(|value| value.checked_add(socket_base))
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WebSocket progress reservation overflowed usize".into())
|
||||
})?;
|
||||
if limits.websocket_bytes_global < minimum_websocket_progress {
|
||||
return config_error(
|
||||
"web.limits.websocket_bytes_global must preserve one socket read and write",
|
||||
);
|
||||
}
|
||||
let data_bytes = limits
|
||||
.pending_bytes_global
|
||||
.saturating_sub(limits.control_bytes_global);
|
||||
let queue_progress = limits
|
||||
.max_body_bytes
|
||||
.checked_add(
|
||||
limits
|
||||
.max_frames_per_body
|
||||
.checked_mul(WEB_QUEUE_ITEM_COST)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WEB queue progress reservation overflowed usize".into())
|
||||
})?,
|
||||
)
|
||||
.and_then(|value| value.checked_add(limits.carrier_batch_bytes))
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WEB queue progress reservation overflowed usize".into())
|
||||
})?;
|
||||
if limits.websocket_bytes_global > data_bytes.saturating_sub(queue_progress) {
|
||||
return config_error("web.limits.websocket_bytes_global must leave bounded queue progress");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user