API for WEB: bounded lifecycle and overload observability added

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-29 16:19:21 +03:00
parent 012dc07a98
commit 084834f5ec
45 changed files with 2440 additions and 298 deletions
+3
View File
@@ -265,6 +265,7 @@ const WEB_CONFIG_KEYS: &[&str] = &[
"carriers",
"carrier_learning",
"carrier_negotiation_aggressiveness",
"http_connection_capacity_action",
"debug",
"limits",
"timeouts",
@@ -278,6 +279,7 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
"carrier_batch_bytes",
"max_frames_per_body",
"max_http_connections",
"max_http_overload_connections",
"max_http_handlers",
"max_lane_open_waits_per_session",
"pending_bytes_per_lane",
@@ -354,6 +356,7 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
"bootstrap_lifetime_secs",
"reconnect_grace_secs",
"http_idle_secs",
"http_overload_timeout_ms",
"shutdown_secs",
"decoy_header_secs",
];
+58
View File
@@ -83,9 +83,59 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> {
timeouts::validate(&config.web.timeouts)?;
websocket::validate(&carriers, &config.web.limits, &config.web.timeouts)?;
validate_vhosts(config)?;
validate_decoy_listener_separation(config)?;
Ok(())
}
/// Rejects a direct decoy recursion into an effective WEB listener.
pub(super) fn validate_decoy_listener_separation(config: &ProxyConfig) -> Result<()> {
let web_listeners = config
.server
.listeners
.iter()
.filter(|listener| listener.transport == ListenerTransport::Web)
.filter(|listener| {
(listener.ip.is_ipv4() && config.network.ipv4)
|| (listener.ip.is_ipv6() && config.network.ipv6 != Some(false))
})
.map(|listener| SocketAddr::new(listener.ip, listener.port.unwrap_or(config.server.port)))
.collect::<Vec<_>>();
for (vhost_idx, vhost) in config.web.vhosts.iter().enumerate() {
let WebDecoyConfig::HttpUpstream { upstream } = &vhost.decoy else {
continue;
};
let parsed = url::Url::parse(upstream).map_err(|error| {
ProxyError::Config(format!(
"web.vhosts[{vhost_idx}].decoy.upstream is invalid: {error}"
))
})?;
let Some(port) = parsed.port_or_known_default() else {
continue;
};
let upstream_ip = match parsed.host() {
Some(url::Host::Ipv4(ip)) => IpAddr::V4(ip),
Some(url::Host::Ipv6(ip)) => IpAddr::V6(ip),
_ => continue,
};
let upstream_addr = SocketAddr::new(upstream_ip, port);
if web_listeners
.iter()
.any(|listener| listener_covers(*listener, upstream_addr))
{
return config_error(&format!(
"web.vhosts[{vhost_idx}].decoy upstream overlaps WEB listener {upstream_addr}"
));
}
}
Ok(())
}
fn listener_covers(listener: SocketAddr, target: SocketAddr) -> bool {
listener.port() == target.port()
&& (listener.ip() == target.ip()
|| (listener.ip().is_unspecified() && listener.is_ipv4() == target.is_ipv4()))
}
fn validate_web_listener(
config: &ProxyConfig,
idx: usize,
@@ -169,6 +219,10 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
let positive = [
("max_http_connections", limits.max_http_connections),
(
"max_http_overload_connections",
limits.max_http_overload_connections,
),
("max_http_handlers", limits.max_http_handlers),
(
"max_lane_open_waits_per_session",
@@ -222,6 +276,10 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
}
for (field, value) in [
("max_http_connections", limits.max_http_connections),
(
"max_http_overload_connections",
limits.max_http_overload_connections,
),
("max_http_handlers", limits.max_http_handlers),
("max_body_readers", limits.max_body_readers),
("max_body_bytes_global", limits.max_body_bytes_global),
+8
View File
@@ -5,6 +5,7 @@ const WEB_DEBUG_STATUS_PAGE_BYTES: usize = 8 * 1024 * 1024;
const WEB_DEBUG_GROUP_SCRATCH_BYTES: usize = 4 * 1024 * 1024;
const WEB_CARRIER_LEARNING_ENTRY_BYTES: usize = 512;
const WEB_LANE_STATE_BYTES: usize = 512;
const WEB_OVERLOAD_CONNECTION_BYTES: usize = 4 * 1024;
/// Validates process-wide body, header, queue, static, and debug reservations.
pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
@@ -30,6 +31,12 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
.ok_or_else(|| {
ProxyError::Config("web.limits HTTP header reservations overflow usize".to_string())
})?;
let overload_connection_reservation = limits
.max_http_overload_connections
.checked_mul(WEB_OVERLOAD_CONNECTION_BYTES)
.ok_or_else(|| {
ProxyError::Config("web.limits HTTP overload reservations overflow usize".to_string())
})?;
let debug_ring_index = limits
.debug_records_capacity
.checked_mul(std::mem::size_of::<usize>())
@@ -77,6 +84,7 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
.and_then(|value| value.checked_add(carrier_learning_reservation))
.and_then(|value| value.checked_add(lane_state_reservation))
.and_then(|value| value.checked_add(http_header_reservation))
.and_then(|value| value.checked_add(overload_connection_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
+3
View File
@@ -2,6 +2,9 @@ use super::*;
/// Validates WEB request, learning, and lifecycle timeouts.
pub(super) fn validate(timeouts: &WebTimeoutsConfig) -> Result<()> {
if !(1..=60_000).contains(&timeouts.http_overload_timeout_ms) {
return config_error("web.timeouts.http_overload_timeout_ms must be within [1, 60000]");
}
let values = [
("header_secs", timeouts.header_secs),
("body_secs", timeouts.body_secs),