mirror of
https://github.com/telemt/telemt.git
synced 2026-09-09 12:04:09 +03:00
WEB: websocket + websocket-lanes as Carrier
This commit is contained in:
+64
-21
@@ -1,6 +1,6 @@
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::proxy::shared_state::ConntrackClosePolicy;
|
||||
@@ -15,10 +15,15 @@ mod tests;
|
||||
|
||||
impl WebSession {
|
||||
/// Starts one owned inner handshake and relay task for an admitted stream.
|
||||
pub(super) fn spawn_stream(self: &Arc<Self>, stream_id: u32, peer_port: u16) {
|
||||
pub(super) fn spawn_stream(
|
||||
self: &Arc<Self>,
|
||||
stream_id: u32,
|
||||
peer_port: u16,
|
||||
retain_reservation_on_reject: bool,
|
||||
) -> bool {
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
self.stream_finished(stream_id, peer_port);
|
||||
return;
|
||||
self.stream_rejected_before_spawn(stream_id, peer_port, retain_reservation_on_reject);
|
||||
return false;
|
||||
};
|
||||
let generation = manager.active_generation();
|
||||
if !*generation.admission_rx.borrow() {
|
||||
@@ -27,8 +32,8 @@ impl WebSession {
|
||||
Some(stream_id),
|
||||
Some("admission_closed"),
|
||||
);
|
||||
self.stream_finished(stream_id, peer_port);
|
||||
return;
|
||||
self.stream_rejected_before_spawn(stream_id, peer_port, retain_reservation_on_reject);
|
||||
return false;
|
||||
}
|
||||
let Ok(connection_permit) = generation.max_connections.clone().try_acquire_owned() else {
|
||||
manager.record_stream_rejected();
|
||||
@@ -37,21 +42,24 @@ impl WebSession {
|
||||
Some(stream_id),
|
||||
Some("connection_limit"),
|
||||
);
|
||||
self.stream_finished(stream_id, peer_port);
|
||||
return;
|
||||
self.stream_rejected_before_spawn(stream_id, peer_port, retain_reservation_on_reject);
|
||||
return false;
|
||||
};
|
||||
let deps = generation.client_runtime_deps();
|
||||
let replay_checker = Arc::clone(&generation.replay_checker);
|
||||
let session = Arc::clone(self);
|
||||
let cancel = self.cancel.clone();
|
||||
let retain_rejected = Arc::new(AtomicBool::new(false));
|
||||
self.tasks_live.fetch_add(1, Ordering::AcqRel);
|
||||
let spawned = generation.spawn_session(async move {
|
||||
let completion = StreamCompletion {
|
||||
session: Arc::clone(&session),
|
||||
stream_id,
|
||||
peer_port,
|
||||
retain_rejected: Arc::clone(&retain_rejected),
|
||||
};
|
||||
let future = async move {
|
||||
let _connection_permit = connection_permit;
|
||||
let _completion = StreamCompletion {
|
||||
session: Arc::clone(&session),
|
||||
stream_id,
|
||||
peer_port,
|
||||
};
|
||||
let _completion = completion;
|
||||
session.trace_lifecycle(
|
||||
crate::web::trace::TraceLifecycleEvent::StreamAdmitted,
|
||||
Some(stream_id),
|
||||
@@ -69,16 +77,41 @@ impl WebSession {
|
||||
peer_port,
|
||||
) => {}
|
||||
}
|
||||
});
|
||||
if !spawned {
|
||||
};
|
||||
if let Err(future) = generation.try_spawn_session(future) {
|
||||
retain_rejected.store(retain_reservation_on_reject, Ordering::Release);
|
||||
self.trace_lifecycle(
|
||||
crate::web::trace::TraceLifecycleEvent::StreamRejected,
|
||||
Some(stream_id),
|
||||
Some("generation_closed"),
|
||||
);
|
||||
self.tasks_live.fetch_sub(1, Ordering::AcqRel);
|
||||
drop(future);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn stream_rejected_before_spawn(
|
||||
&self,
|
||||
stream_id: u32,
|
||||
peer_port: u16,
|
||||
retain_reservation: bool,
|
||||
) {
|
||||
if !retain_reservation {
|
||||
self.stream_finished(stream_id, peer_port);
|
||||
self.tasks_done.notify_waiters();
|
||||
return;
|
||||
}
|
||||
let queued = {
|
||||
let mut state = self.state.lock();
|
||||
state.streams.remove(&stream_id).map(|stream| {
|
||||
let (bytes, items) = inbound_queue_cost(&stream.inbound);
|
||||
self.release_locked(&mut state, bytes, items, false);
|
||||
self.remember_closed_locked(&mut state, stream_id);
|
||||
self.queue_control_locked(&mut state, FrameType::Close, stream_id, &[])
|
||||
})
|
||||
};
|
||||
if queued.is_some_and(|queued| !queued) {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +139,7 @@ impl WebSession {
|
||||
if !queued {
|
||||
self.close();
|
||||
}
|
||||
if self.carrier() == crate::config::WebCarrier::Https {
|
||||
if self.carrier().is_multiplexed() {
|
||||
self.down_notify.notify_waiters();
|
||||
}
|
||||
}
|
||||
@@ -117,6 +150,7 @@ struct StreamCompletion {
|
||||
session: Arc<WebSession>,
|
||||
stream_id: u32,
|
||||
peer_port: u16,
|
||||
retain_rejected: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for StreamCompletion {
|
||||
@@ -126,7 +160,12 @@ impl Drop for StreamCompletion {
|
||||
Some(self.stream_id),
|
||||
None,
|
||||
);
|
||||
self.session.stream_finished(self.stream_id, self.peer_port);
|
||||
if self.retain_rejected.load(Ordering::Acquire) {
|
||||
self.session
|
||||
.stream_rejected_before_spawn(self.stream_id, self.peer_port, true);
|
||||
} else {
|
||||
self.session.stream_finished(self.stream_id, self.peer_port);
|
||||
}
|
||||
if self.session.tasks_live.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
self.session.tasks_done.notify_waiters();
|
||||
}
|
||||
@@ -263,6 +302,10 @@ async fn run_stream(
|
||||
session.trace_lifecycle(
|
||||
crate::web::trace::TraceLifecycleEvent::RelayEnded,
|
||||
Some(stream_id),
|
||||
Some(if relay_result.is_ok() { "completed" } else { "error" }),
|
||||
Some(if relay_result.is_ok() {
|
||||
"completed"
|
||||
} else {
|
||||
"error"
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ use crate::config::{
|
||||
WebSecretMode,
|
||||
};
|
||||
use crate::crypto::{AesCtr, sha256};
|
||||
use crate::maestro::generation::{
|
||||
RuntimeGeneration, test_runtime_generation_with_admission,
|
||||
};
|
||||
use crate::maestro::generation::{RuntimeGeneration, test_runtime_generation_with_admission};
|
||||
use crate::protocol::constants::{
|
||||
DC_IDX_POS, HANDSHAKE_LEN, IV_LEN, PREKEY_LEN, PROTO_TAG_POS, ProtoTag, SKIP_LEN,
|
||||
};
|
||||
@@ -39,10 +37,11 @@ impl TestRuntime {
|
||||
) -> Result<u64, ManagerError> {
|
||||
let encoded = frame::encode(frame_type, stream_id, payload);
|
||||
match self.session.carrier() {
|
||||
WebCarrier::Https => self.session.process_up(sequence, &encoded),
|
||||
WebCarrier::HttpsLanes => {
|
||||
self.session
|
||||
.process_up_lane(stream_id, sequence, &encoded)
|
||||
WebCarrier::Https | WebCarrier::Websocket => {
|
||||
self.session.process_up(sequence, &encoded)
|
||||
}
|
||||
WebCarrier::HttpsLanes | WebCarrier::WebsocketLanes => {
|
||||
self.session.process_up_lane(stream_id, sequence, &encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +151,7 @@ fn valid_plain_handshake() -> [u8; HANDSHAKE_LEN] {
|
||||
let mut cipher = AesCtr::new(&dec_key, u128::from_be_bytes(dec_iv));
|
||||
let keystream = cipher.encrypt(&[0u8; HANDSHAKE_LEN]);
|
||||
let mut plaintext = [0u8; HANDSHAKE_LEN];
|
||||
plaintext[PROTO_TAG_POS..PROTO_TAG_POS + 4]
|
||||
.copy_from_slice(&ProtoTag::Intermediate.to_bytes());
|
||||
plaintext[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&ProtoTag::Intermediate.to_bytes());
|
||||
plaintext[DC_IDX_POS..DC_IDX_POS + 2].copy_from_slice(&2i16.to_le_bytes());
|
||||
for index in PROTO_TAG_POS..HANDSHAKE_LEN {
|
||||
handshake[index] = plaintext[index] ^ keystream[index];
|
||||
|
||||
@@ -5,14 +5,13 @@ use bytes::{BufMut, Bytes, BytesMut};
|
||||
use super::{
|
||||
DownBatch, PendingClass, PollResult, QUEUE_ITEM_COST, QueuedFrame, SessionState, WebSession,
|
||||
};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame::{self, FrameType};
|
||||
use crate::web::manager::ManagerError;
|
||||
|
||||
impl WebSession {
|
||||
/// Polls pending downlink frames with cursor replay and newest-poll-wins semantics.
|
||||
pub(crate) async fn poll_down(&self, cursor: u64) -> Result<PollResult, ManagerError> {
|
||||
if self.carrier() != WebCarrier::Https {
|
||||
if !self.carrier().is_multiplexed() {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let epoch = {
|
||||
@@ -167,7 +166,13 @@ impl WebSession {
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
if !manager.try_reserve_pending(bytes, items, control, class == PendingClass::Downlink) {
|
||||
if !manager.try_reserve_pending(
|
||||
self.profile_key,
|
||||
bytes,
|
||||
items,
|
||||
control,
|
||||
class == PendingClass::Downlink,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
state.pending_bytes += bytes;
|
||||
@@ -194,7 +199,7 @@ impl WebSession {
|
||||
state.pending_control_items = state.pending_control_items.saturating_sub(items);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.release_pending(bytes, items, control);
|
||||
manager.release_pending(self.profile_key, bytes, items, control);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +213,7 @@ impl WebSession {
|
||||
if amount == 0 {
|
||||
return true;
|
||||
}
|
||||
if self.carrier() == WebCarrier::HttpsLanes {
|
||||
if self.carrier().uses_lanes() {
|
||||
return self.queue_control_locked(
|
||||
state,
|
||||
FrameType::Window,
|
||||
@@ -257,7 +262,7 @@ impl WebSession {
|
||||
stream_id: u32,
|
||||
payload: &[u8],
|
||||
) -> bool {
|
||||
if self.carrier() == WebCarrier::HttpsLanes {
|
||||
if self.carrier().uses_lanes() {
|
||||
return self.queue_frame_locked(state, FrameType::Data, stream_id, payload, false);
|
||||
}
|
||||
let can_coalesce = state.pending_frames.back().is_some_and(|last| {
|
||||
@@ -290,7 +295,7 @@ impl WebSession {
|
||||
payload: &[u8],
|
||||
control: bool,
|
||||
) -> bool {
|
||||
if self.carrier() == WebCarrier::HttpsLanes {
|
||||
if self.carrier().uses_lanes() {
|
||||
return self.queue_lane_frame_locked(state, frame_type, stream_id, payload, control);
|
||||
}
|
||||
let cost = frame::HEADER_BYTES + payload.len() + QUEUE_ITEM_COST;
|
||||
@@ -409,7 +414,9 @@ mod tests {
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::{WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig};
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
fn session() -> Arc<WebSession> {
|
||||
|
||||
@@ -113,6 +113,7 @@ impl WebSession {
|
||||
&mut state,
|
||||
&frames,
|
||||
&mut opened,
|
||||
&mut None,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
);
|
||||
@@ -137,7 +138,7 @@ impl WebSession {
|
||||
return result;
|
||||
}
|
||||
for (stream_id, peer_port) in opened {
|
||||
self.spawn_stream(stream_id, peer_port);
|
||||
self.spawn_stream(stream_id, peer_port, false);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
@@ -151,7 +152,7 @@ impl WebSession {
|
||||
lane_id: u32,
|
||||
cursor: u64,
|
||||
) -> Result<PollResult, ManagerError> {
|
||||
if self.carrier() != WebCarrier::HttpsLanes || lane_id > frame::MAX_STREAM_ID {
|
||||
if !self.carrier().uses_lanes() || lane_id > frame::MAX_STREAM_ID {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let (epoch, notify) = {
|
||||
@@ -404,7 +405,7 @@ impl WebSession {
|
||||
|
||||
pub(super) fn remember_closed_locked(&self, state: &mut SessionState, stream_id: u32) {
|
||||
let evicted = remember_closed(state, stream_id, self.limits.max_tombstones_per_session);
|
||||
if self.carrier() != WebCarrier::HttpsLanes {
|
||||
if !self.carrier().uses_lanes() {
|
||||
return;
|
||||
}
|
||||
if let Some(evicted) = evicted {
|
||||
@@ -415,7 +416,7 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn release_lane_locked(&self, state: &mut SessionState, lane_id: u32) {
|
||||
pub(super) fn release_lane_locked(&self, state: &mut SessionState, lane_id: u32) {
|
||||
let Some(mut lane) = state.carrier_lanes.remove(&lane_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ use super::{
|
||||
InboundChunk, PendingClass, QUEUE_ITEM_COST, SessionState, StreamState, WebSession,
|
||||
inbound_queue_cost,
|
||||
};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
|
||||
@@ -22,7 +21,7 @@ impl WebSession {
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
if self.carrier() != WebCarrier::Https {
|
||||
if !self.carrier().is_multiplexed() {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if self
|
||||
@@ -90,6 +89,7 @@ impl WebSession {
|
||||
&mut state,
|
||||
&frames,
|
||||
&mut opened,
|
||||
&mut None,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
);
|
||||
@@ -113,7 +113,7 @@ impl WebSession {
|
||||
return result;
|
||||
}
|
||||
for (stream_id, peer_port) in opened {
|
||||
self.spawn_stream(stream_id, peer_port);
|
||||
self.spawn_stream(stream_id, peer_port, false);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
@@ -126,6 +126,7 @@ impl WebSession {
|
||||
state: &mut SessionState,
|
||||
frames: &[Frame<'_>],
|
||||
opened: &mut Vec<(u32, u16)>,
|
||||
reserved_open: &mut Option<(u32, u16)>,
|
||||
unused_bytes: &mut usize,
|
||||
unused_items: &mut usize,
|
||||
) -> bool {
|
||||
@@ -136,13 +137,31 @@ impl WebSession {
|
||||
let was_closed = state.closed_streams.contains(&value.stream_id);
|
||||
match value.frame_type {
|
||||
FrameType::Open => {
|
||||
let Some(peer_port) = self.reserve_stream_locked(state) else {
|
||||
self.remember_closed_locked(state, value.stream_id);
|
||||
if !self.queue_control_locked(state, FrameType::Close, value.stream_id, &[])
|
||||
let peer_port = match reserved_open.take() {
|
||||
Some((reserved_stream_id, peer_port))
|
||||
if reserved_stream_id == value.stream_id =>
|
||||
{
|
||||
peer_port
|
||||
}
|
||||
Some(reserved) => {
|
||||
*reserved_open = Some(reserved);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
None => {
|
||||
let Some(peer_port) = self.reserve_stream_locked(state) else {
|
||||
self.remember_closed_locked(state, value.stream_id);
|
||||
if !self.queue_control_locked(
|
||||
state,
|
||||
FrameType::Close,
|
||||
value.stream_id,
|
||||
&[],
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
peer_port
|
||||
}
|
||||
};
|
||||
state.streams.insert(
|
||||
value.stream_id,
|
||||
@@ -331,7 +350,9 @@ mod tests {
|
||||
use super::*;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::config::{WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig};
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
fn session() -> Arc<WebSession> {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::uplink::{inbound_reservation, validate_batch};
|
||||
use super::{CarrierLane, PendingClass, WebSession, inbound_queue_cost};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame;
|
||||
use crate::web::manager::ManagerError;
|
||||
|
||||
/// Pre-OPEN stream quota and synthetic tuple ownership for one WebSocket lane.
|
||||
pub(crate) struct WebSocketLaneReservation {
|
||||
session: Arc<WebSession>,
|
||||
lane_id: u32,
|
||||
peer_port: u16,
|
||||
transferred: bool,
|
||||
}
|
||||
|
||||
impl WebSocketLaneReservation {
|
||||
/// Returns the logical stream owned by this connection.
|
||||
pub(crate) fn lane_id(&self) -> u32 {
|
||||
self.lane_id
|
||||
}
|
||||
|
||||
fn transfer_to_stream(&mut self) {
|
||||
let removed = self
|
||||
.session
|
||||
.state
|
||||
.lock()
|
||||
.websocket_lane_reservations
|
||||
.remove(&self.lane_id);
|
||||
if removed == Some(self.peer_port) {
|
||||
self.transferred = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebSocketLaneReservation {
|
||||
fn drop(&mut self) {
|
||||
if !self.transferred {
|
||||
self.session
|
||||
.release_websocket_lane_reservation(self.lane_id, self.peer_port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Acquires stream quota and tuple ownership before a lane returns HTTP 101.
|
||||
pub(crate) fn reserve_websocket_lane(
|
||||
self: &Arc<Self>,
|
||||
lane_id: u32,
|
||||
) -> Result<WebSocketLaneReservation, ManagerError> {
|
||||
if self.carrier() != WebCarrier::WebsocketLanes
|
||||
|| lane_id == 0
|
||||
|| lane_id > frame::MAX_STREAM_ID
|
||||
{
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.active_peer_ports.len() >= self.profile.max_streams_per_session
|
||||
|| state.streams.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.websocket_lane_reservations.contains_key(&lane_id)
|
||||
{
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(peer_port) = manager.try_acquire_stream(
|
||||
self.profile_key,
|
||||
self.profile.max_streams,
|
||||
self.client_ip,
|
||||
self.profile.public_addr,
|
||||
) else {
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
if !state.active_peer_ports.insert(peer_port) {
|
||||
manager.release_stream(
|
||||
self.profile_key,
|
||||
self.client_ip,
|
||||
self.profile.public_addr,
|
||||
peer_port,
|
||||
);
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
state.websocket_lane_reservations.insert(lane_id, peer_port);
|
||||
state.carrier_lanes.insert(lane_id, CarrierLane::new());
|
||||
Ok(WebSocketLaneReservation {
|
||||
session: Arc::clone(self),
|
||||
lane_id,
|
||||
peer_port,
|
||||
transferred: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies one ordered WebSocket lane message without closing sibling lanes.
|
||||
pub(crate) fn process_websocket_lane(
|
||||
self: &Arc<Self>,
|
||||
reservation: &mut WebSocketLaneReservation,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<(), ManagerError> {
|
||||
if !Arc::ptr_eq(self, &reservation.session)
|
||||
|| reservation.lane_id == 0
|
||||
|| reservation.lane_id > frame::MAX_STREAM_ID
|
||||
{
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let lane_id = reservation.lane_id;
|
||||
let frames = frame::parse_all(body, &self.limits).map_err(|_| ManagerError::Protocol)?;
|
||||
if frames
|
||||
.iter()
|
||||
.copied()
|
||||
.any(|value| value.stream_id != lane_id || frame::validate_client_shape(value).is_err())
|
||||
{
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let digest = Sha256::digest(body).into();
|
||||
let mut opened = Vec::new();
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if !reservation.transferred
|
||||
&& state.websocket_lane_reservations.get(&lane_id) != Some(&reservation.peer_port)
|
||||
{
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
if sequence == 0 || sequence != lane.last_up_sequence.saturating_add(1) {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if lane.up_active {
|
||||
return Err(ManagerError::Concurrent);
|
||||
}
|
||||
lane.up_active = true;
|
||||
if !validate_batch(&state, &frames) {
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
}
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let (reserve_bytes, reserve_items) = inbound_reservation(&state, &frames);
|
||||
if !self.reserve_locked(
|
||||
&mut state,
|
||||
reserve_bytes,
|
||||
reserve_items,
|
||||
PendingClass::Uplink,
|
||||
) {
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
}
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
let mut unused_bytes = reserve_bytes;
|
||||
let mut unused_items = reserve_items;
|
||||
let mut reserved_open =
|
||||
(!reservation.transferred).then_some((lane_id, reservation.peer_port));
|
||||
let applied = self.apply_batch_locked(
|
||||
&mut state,
|
||||
&frames,
|
||||
&mut opened,
|
||||
&mut reserved_open,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
);
|
||||
self.release_locked(&mut state, unused_bytes, unused_items, false);
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
if applied {
|
||||
lane.last_up_sequence = sequence;
|
||||
lane.last_up_digest = digest;
|
||||
}
|
||||
}
|
||||
state.last_activity = Instant::now();
|
||||
applied.then_some(()).ok_or(ManagerError::Protocol)
|
||||
};
|
||||
result?;
|
||||
for (stream_id, peer_port) in opened {
|
||||
if stream_id != lane_id || peer_port != reservation.peer_port {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if !self.spawn_stream(stream_id, peer_port, true) {
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
reservation.transfer_to_stream();
|
||||
}
|
||||
if !reservation.transferred {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ends one failed or disconnected lane without closing its parent session.
|
||||
pub(crate) fn close_websocket_lane(&self, lane_id: u32) {
|
||||
let reserved = {
|
||||
let mut state = self.state.lock();
|
||||
let reserved = state.websocket_lane_reservations.remove(&lane_id);
|
||||
if let Some(stream) = state.streams.remove(&lane_id) {
|
||||
let (bytes, items) = inbound_queue_cost(&stream.inbound);
|
||||
self.release_locked(&mut state, bytes, items, false);
|
||||
if let Some(waker) = stream.read_waker {
|
||||
waker.wake();
|
||||
}
|
||||
if let Some(waker) = stream.write_waker {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
self.remember_closed_locked(&mut state, lane_id);
|
||||
self.release_lane_locked(&mut state, lane_id);
|
||||
reserved
|
||||
};
|
||||
if let Some(peer_port) = reserved {
|
||||
self.release_websocket_lane_reservation(lane_id, peer_port);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_websocket_lane_reservation(&self, lane_id: u32, peer_port: u16) {
|
||||
let removed = {
|
||||
let mut state = self.state.lock();
|
||||
if state.websocket_lane_reservations.get(&lane_id) == Some(&peer_port) {
|
||||
state.websocket_lane_reservations.remove(&lane_id);
|
||||
}
|
||||
self.release_lane_locked(&mut state, lane_id);
|
||||
state.active_peer_ports.remove(&peer_port)
|
||||
};
|
||||
if removed && let Some(manager) = self.manager.upgrade() {
|
||||
manager.release_stream(
|
||||
self.profile_key,
|
||||
self.client_ip,
|
||||
self.profile.public_addr,
|
||||
peer_port,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,135 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use super::*;
|
||||
use crate::config::{ProxyConfig, WebRuntimeConfig, WebRuntimeProfile, WebSecretMode};
|
||||
use crate::maestro::generation::{RuntimeGeneration, test_runtime_generation_with_admission};
|
||||
use crate::web::frame::FrameType;
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
struct TestRuntime {
|
||||
session: Arc<WebSession>,
|
||||
manager: Arc<WebProcessRuntime>,
|
||||
generation: Arc<RuntimeGeneration>,
|
||||
}
|
||||
|
||||
impl TestRuntime {
|
||||
async fn shutdown(self) {
|
||||
self.session.close();
|
||||
self.session.wait().await;
|
||||
self.manager.shutdown().await;
|
||||
self.generation.stop_sessions().await;
|
||||
self.generation.stop_background_tasks().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime(admission: bool) -> TestRuntime {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: "203.0.113.10:443".parse().unwrap(),
|
||||
user: "default".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::WebsocketLanes,
|
||||
capability: [7; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 2,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
let mut config = ProxyConfig::default();
|
||||
config.web.enabled = true;
|
||||
config.web.carrier = WebCarrier::WebsocketLanes;
|
||||
config.web.timeouts.shutdown_secs = 1;
|
||||
config.web.runtime = Some(Arc::new(WebRuntimeConfig {
|
||||
vhosts: BTreeMap::new(),
|
||||
profiles: vec![Arc::clone(&profile)],
|
||||
}));
|
||||
config.rebuild_runtime_user_auth().unwrap();
|
||||
let limits = config.web.limits.clone();
|
||||
let timeouts = config.web.timeouts.clone();
|
||||
let (_admission_tx, admission_rx) = watch::channel(admission);
|
||||
let generation = test_runtime_generation_with_admission(1, config, admission_rx);
|
||||
let manager = WebProcessRuntime::start(Arc::new(ArcSwap::from(Arc::clone(&generation))));
|
||||
let session = WebSession::new(
|
||||
Arc::downgrade(&manager),
|
||||
[8; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[7; 32],
|
||||
limits,
|
||||
timeouts,
|
||||
);
|
||||
TestRuntime {
|
||||
session,
|
||||
manager,
|
||||
generation,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejected_open_retains_stream_quota_until_lane_socket_teardown() {
|
||||
let runtime = runtime(false);
|
||||
let mut reservation = runtime.session.reserve_websocket_lane(7).unwrap();
|
||||
let open = frame::encode(FrameType::Open, 7, &[]);
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.session
|
||||
.process_websocket_lane(&mut reservation, 1, &open),
|
||||
Err(ManagerError::Limit),
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.manager
|
||||
.try_acquire_stream(
|
||||
runtime.session.profile_key,
|
||||
runtime.session.profile.max_streams,
|
||||
runtime.session.client_ip,
|
||||
runtime.session.profile.public_addr,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
|
||||
runtime.session.close_websocket_lane(7);
|
||||
drop(reservation);
|
||||
let peer_port = runtime
|
||||
.manager
|
||||
.try_acquire_stream(
|
||||
runtime.session.profile_key,
|
||||
runtime.session.profile.max_streams,
|
||||
runtime.session.client_ip,
|
||||
runtime.session.profile.public_addr,
|
||||
)
|
||||
.unwrap();
|
||||
runtime.manager.release_stream(
|
||||
runtime.session.profile_key,
|
||||
runtime.session.client_ip,
|
||||
runtime.session.profile.public_addr,
|
||||
peer_port,
|
||||
);
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_lane_message_does_not_close_sibling_session_state() {
|
||||
let runtime = runtime(true);
|
||||
let mut reservation = runtime.session.reserve_websocket_lane(7).unwrap();
|
||||
let data = frame::encode(FrameType::Data, 7, &[1]);
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.session
|
||||
.process_websocket_lane(&mut reservation, 1, &data),
|
||||
Err(ManagerError::Protocol),
|
||||
);
|
||||
assert!(!runtime.session.state.lock().closed);
|
||||
|
||||
runtime.session.close_websocket_lane(7);
|
||||
drop(reservation);
|
||||
assert!(runtime.session.reserve_websocket_lane(8).is_ok());
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
Reference in New Issue
Block a user