WEB Carriers negotiation and lane lifecycle bounds hardened

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-26 23:08:53 +03:00
parent 34eeb2d856
commit e72b1f04d1
15 changed files with 645 additions and 489 deletions
+5 -198
View File
@@ -1,6 +1,6 @@
use std::convert::Infallible;
use std::error::Error;
use std::net::{IpAddr, SocketAddr};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -19,7 +19,6 @@ use tokio_util::sync::CancellationToken;
use crate::config::{WebClientIpSource, WebRuntimeVhost};
use crate::web::bridge;
use crate::web::frame::{self, FrameType};
use crate::web::manager::{ManagerError, WebProcessRuntime};
// Response-body activity keeps connection idle accounting lifecycle-correct.
@@ -34,6 +33,8 @@ mod down;
mod request;
// Carrier response construction and lane-header helpers are shared by handlers.
mod response;
// Session creation and replacement negotiation remain separate from request routing.
mod session;
// RFC 6455 upgrade validation and carrier drivers remain isolated from HTTP routing.
#[cfg(test)]
mod tests;
@@ -50,19 +51,18 @@ use decoy::serve_decoy;
use down::handle_down;
use request::{
bearer_token_hash, binary_content_type, bridge_candidate, canonical_request_host,
canonical_u64_header, carrier_ip_learning_eligible, carrier_request, client_ip,
compatible_cookie_header, match_profile,
canonical_u64_header, client_ip, compatible_cookie_header, match_profile,
};
use response::{
bad_gateway, carrier_empty, carrier_headers, carrier_lane, full_response, generic_not_found,
insert_header, service_unavailable,
};
use session::handle_session;
type BoxError = Box<dyn Error + Send + Sync>;
type HttpBody = UnsyncBoxBody<Bytes, BoxError>;
type HttpResponse = Response<HttpBody>;
const CREATE_BODY_LIMIT: usize = 64;
const TRANSPORT_PATHS: [&str; 3] = ["/api/v1/session", "/api/v1/up", "/api/v1/down"];
const WEBSOCKET_PATH: &str = "/api/v1/ws";
@@ -317,199 +317,6 @@ async fn handle_api(
}
}
async fn handle_session(
request: Request<RequestBody>,
runtime: Arc<WebProcessRuntime>,
vhost: Arc<WebRuntimeVhost>,
token_hash: crate::web::manager::TokenHash,
client_ip: IpAddr,
) -> HttpResponse {
if request.headers().contains_key("x-lane-id") {
return serve_decoy(request, vhost, true, &runtime).await;
}
if request.method() == Method::DELETE {
if request.headers().contains_key(header::CONTENT_TYPE) {
return serve_decoy(request, vhost, true, &runtime).await;
}
if let Some(trace) = request_trace(&request)
&& let Ok(session) = runtime.get_session(token_hash, &vhost.host)
{
trace.set_route(TraceRoute::Session);
trace.bind_identity(session.trace_identity());
}
let CollectedBody {
request,
body,
_body_budget,
} = match collect_body(request, &runtime, 1, true).await {
Ok(result) => result,
Err(CollectBodyError::Limit) => return service_unavailable(),
Err(CollectBodyError::Invalid(request)) => {
return serve_decoy(request, vhost, true, &runtime).await;
}
};
if !body.is_empty() || runtime.close_token(token_hash, &vhost.host).is_err() {
return serve_decoy(request, vhost, true, &runtime).await;
}
return carrier_empty(StatusCode::NO_CONTENT);
}
if request.method() != Method::POST || !binary_content_type(&request) {
return serve_decoy(request, vhost, true, &runtime).await;
}
let Some(carrier_request) = carrier_request(&request, &vhost.host) else {
return serve_decoy(request, vhost, true, &runtime).await;
};
let ip_learning_eligible = carrier_ip_learning_eligible(&request, client_ip);
let Some((trace_session_id, profile)) =
runtime.bootstrap_trace_identity(token_hash, &vhost.host)
else {
return serve_decoy(request, vhost, true, &runtime).await;
};
if let Some(trace) = request_trace(&request) {
trace.set_route(TraceRoute::Session);
trace.bind_profile(&profile, trace_session_id);
}
let CollectedBody {
request,
body,
_body_budget,
} = match collect_body(request, &runtime, CREATE_BODY_LIMIT, false).await {
Ok(result) => result,
Err(CollectBodyError::Limit) => return service_unavailable(),
Err(CollectBodyError::Invalid(request)) => {
return serve_decoy(request, vhost, true, &runtime).await;
}
};
if let Some(trace) = request_trace(&request) {
trace.record_frames(
TraceDirection::Request,
&body,
&runtime.active_generation().config().web.limits,
);
}
match runtime.create_session(
token_hash,
&vhost.host,
client_ip,
&body,
carrier_request,
ip_learning_eligible,
) {
Ok(result) => {
let welcome = frame::encode(FrameType::Welcome, 0, &[]);
if let Some(trace) = request_trace(&request) {
trace.register_redaction(result.token.as_bytes());
trace.record_frames(
TraceDirection::Response,
&welcome,
&runtime.active_generation().config().web.limits,
);
}
let mut response = full_response(StatusCode::OK, welcome);
carrier_headers(&mut response);
insert_header(
&mut response,
HeaderName::from_static("x-session-token"),
&result.token,
);
response.headers_mut().insert(
HeaderName::from_static("x-carrier-mode"),
HeaderValue::from_static(result.carrier.as_str()),
);
response.headers_mut().insert(
HeaderName::from_static("x-down-cursor"),
HeaderValue::from_static("0"),
);
if let Some(attempt) = result.attempt {
insert_header(
&mut response,
HeaderName::from_static("x-carrier-attempt"),
&attempt.to_string(),
);
}
if let Some(candidate_count) = result.candidate_count {
insert_header(
&mut response,
HeaderName::from_static("x-carrier-candidate-count"),
&candidate_count.to_string(),
);
insert_header(
&mut response,
HeaderName::from_static("x-carrier-deadline"),
&result.deadline_secs.unwrap_or_default().to_string(),
);
if let Some(state) = result.carrier_state {
insert_header(
&mut response,
HeaderName::from_static("x-carrier-state"),
state,
);
}
}
response
}
Err(ManagerError::Committed) => {
let mut response = carrier_empty(StatusCode::CONFLICT);
if let Some(echo) = runtime.carrier_echo(
token_hash,
&vhost.host,
client_ip,
carrier_request,
) {
response.headers_mut().insert(
HeaderName::from_static("x-carrier-mode"),
HeaderValue::from_static(echo.carrier.as_str()),
);
insert_header(
&mut response,
HeaderName::from_static("x-carrier-attempt"),
&echo.attempt.to_string(),
);
insert_header(
&mut response,
HeaderName::from_static("x-carrier-candidate-count"),
&echo.candidate_count.to_string(),
);
insert_header(
&mut response,
HeaderName::from_static("x-carrier-deadline"),
&echo.deadline_secs.to_string(),
);
insert_header(
&mut response,
HeaderName::from_static("x-carrier-state"),
echo.state,
);
}
response
}
Err(
error @ (ManagerError::Limit | ManagerError::Backpressure | ManagerError::Concurrent),
) => {
runtime.trace().record_profile_lifecycle(
client_ip,
Some(trace_session_id),
&profile,
TraceLifecycleEvent::SessionRejected,
None,
Some(error.as_str()),
);
service_unavailable()
}
Err(error) => {
runtime.trace().record_profile_lifecycle(
client_ip,
Some(trace_session_id),
&profile,
TraceLifecycleEvent::SessionRejected,
None,
Some(error.as_str()),
);
serve_decoy(request, vhost, true, &runtime).await
}
}
}
async fn handle_up(
request: Request<RequestBody>,
runtime: Arc<WebProcessRuntime>,