WEB: websocket + websocket-lanes as Carrier

This commit is contained in:
Alexey
2026-08-26 09:16:06 +03:00
parent 8e577ec5ca
commit d2edd90479
50 changed files with 3642 additions and 350 deletions
+1 -2
View File
@@ -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",
+10 -1
View File
@@ -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",
+9
View File
@@ -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),
+3 -1
View File
@@ -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
+1 -3
View File
@@ -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)
+83
View File
@@ -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(())
}