mirror of
https://github.com/telemt/telemt.git
synced 2026-09-19 17:00:36 +03:00
WEB Carriers negotiation and lane lifecycle bounds hardened
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
+14
-11
@@ -118,11 +118,11 @@ impl WebSession {
|
||||
.then(|| state.streams.remove(&stream.id))
|
||||
.flatten()
|
||||
.map(|stream_state| {
|
||||
let (bytes, items) = inbound_queue_cost(&stream_state.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, &[])
|
||||
})
|
||||
let (bytes, items) = inbound_queue_cost(&stream_state.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();
|
||||
@@ -137,12 +137,15 @@ impl WebSession {
|
||||
.streams
|
||||
.get(&stream.id)
|
||||
.is_some_and(|state| state.instance == stream.instance);
|
||||
let queued = current.then(|| state.streams.remove(&stream.id)).flatten().map(|stream_state| {
|
||||
let (bytes, items) = inbound_queue_cost(&stream_state.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, &[])
|
||||
});
|
||||
let queued = current
|
||||
.then(|| state.streams.remove(&stream.id))
|
||||
.flatten()
|
||||
.map(|stream_state| {
|
||||
let (bytes, items) = inbound_queue_cost(&stream_state.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 state.closing_streams.get(&stream.id) == Some(&stream.instance) {
|
||||
state.closing_streams.remove(&stream.id);
|
||||
self.remember_closed_locked(&mut state, stream.id);
|
||||
|
||||
@@ -373,3 +373,17 @@ async fn cancellation_while_waiting_for_data_releases_stream_ownership() {
|
||||
);
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exhausted_stream_identity_does_not_acquire_synthetic_port_ownership() {
|
||||
let runtime = test_runtime(WebCarrier::Https, 1);
|
||||
runtime.session.state.lock().next_stream_instance = u64::MAX;
|
||||
|
||||
assert_eq!(
|
||||
runtime.process_frame(1, 1, FrameType::Open, &[]),
|
||||
Err(ManagerError::Closed)
|
||||
);
|
||||
assert!(runtime.session.state.lock().active_peer_ports.is_empty());
|
||||
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,14 @@ impl WebSession {
|
||||
.is_some_and(|value| value.frame_type != FrameType::Open)
|
||||
&& only_late_frames(&frames)
|
||||
{
|
||||
return Ok(sequence);
|
||||
return if self.automatic_carrier
|
||||
&& state.negotiation_phase
|
||||
!= super::SessionNegotiationPhase::Committed
|
||||
{
|
||||
Err(ManagerError::Backpressure)
|
||||
} else {
|
||||
Ok(sequence)
|
||||
};
|
||||
}
|
||||
if lane_id == 0
|
||||
|| frames
|
||||
@@ -160,6 +167,10 @@ impl WebSession {
|
||||
drop(opened);
|
||||
return result;
|
||||
}
|
||||
if self.automatic_carrier && !self.is_carrier_committed() {
|
||||
self.lane_open_notify.notify_waiters();
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use tokio::sync::OwnedSemaphorePermit;
|
||||
|
||||
use super::lane_downlink::take_lane_down_batch;
|
||||
use super::{
|
||||
PendingClass, PollResult, QUEUE_ITEM_COST, QueuedFrame, SessionState, WebSession,
|
||||
|
||||
@@ -20,6 +20,14 @@ fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
fn new_session(
|
||||
limits: WebLimitsConfig,
|
||||
manager: std::sync::Weak<WebProcessRuntime>,
|
||||
) -> Arc<WebSession> {
|
||||
new_session_with_automatic(limits, manager, false)
|
||||
}
|
||||
|
||||
fn new_session_with_automatic(
|
||||
limits: WebLimitsConfig,
|
||||
manager: std::sync::Weak<WebProcessRuntime>,
|
||||
automatic: bool,
|
||||
) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
@@ -48,9 +56,13 @@ fn new_session(
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
if automatic {
|
||||
crate::web::manager::CarrierClientClass::Bridge
|
||||
} else {
|
||||
crate::web::manager::CarrierClientClass::Legacy
|
||||
},
|
||||
None,
|
||||
false,
|
||||
automatic,
|
||||
limits,
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
@@ -259,3 +271,20 @@ fn tombstone_eviction_releases_lane_budget_and_accepts_late_frames() {
|
||||
assert_eq!(session.process_up_lane(7, 7, &late), Ok(7));
|
||||
assert!(!session.state.lock().closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_lane_does_not_ack_a_missing_lane_without_real_progress() {
|
||||
let session = new_session_with_automatic(
|
||||
WebLimitsConfig::default(),
|
||||
std::sync::Weak::new(),
|
||||
true,
|
||||
);
|
||||
let late = frame::encode(FrameType::Data, 7, b"late");
|
||||
|
||||
assert_eq!(
|
||||
session.process_up_lane(7, 1, &late),
|
||||
Err(ManagerError::Backpressure)
|
||||
);
|
||||
assert!(!session.is_carrier_committed());
|
||||
assert!(!session.state.lock().closed);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl WebSession {
|
||||
|
||||
/// Publishes the already-linearized session commit to process state.
|
||||
pub(super) fn finish_carrier_commit(&self) {
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
let published = self.manager.upgrade().is_some_and(|manager| {
|
||||
manager.carrier_committed(
|
||||
self.bootstrap_hash,
|
||||
self.token_hash,
|
||||
@@ -40,7 +40,22 @@ impl WebSession {
|
||||
self.carrier_class,
|
||||
self.client_ip,
|
||||
self.trace_identity(),
|
||||
);
|
||||
)
|
||||
});
|
||||
if !published {
|
||||
return;
|
||||
}
|
||||
let healthy = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || state.negotiation_phase != SessionNegotiationPhase::Committed {
|
||||
false
|
||||
} else {
|
||||
state.carrier_commit_published = true;
|
||||
self.carrier_health_ready_locked(&mut state, Instant::now())
|
||||
}
|
||||
};
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +112,9 @@ impl WebSession {
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
if !self.automatic_carrier
|
||||
|| state.closed
|
||||
|| state.negotiation_phase != SessionNegotiationPhase::Committed
|
||||
|| !state.carrier_commit_published
|
||||
|| state.carrier_health_reported
|
||||
|| state.carrier_health_due_at.is_none_or(|due| now < due)
|
||||
{
|
||||
@@ -233,6 +250,7 @@ mod tests {
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
@@ -251,6 +269,7 @@ mod tests {
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.websocket_carrier_active = true;
|
||||
state.websocket_commit_ack_owner = Some(7);
|
||||
@@ -261,6 +280,21 @@ mod tests {
|
||||
assert!(session.carrier_health_ready_locked(&mut state, now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_waits_for_manager_commit_publication() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
|
||||
assert!(!session.carrier_health_ready_locked(&mut state, now));
|
||||
assert!(!state.carrier_health_reported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_and_supersede_have_one_session_lock_winner() {
|
||||
let committed = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
|
||||
@@ -34,8 +34,11 @@ impl WebSession {
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
self.process_up_inner(sequence, body)
|
||||
.map(|(acknowledged, _)| acknowledged)
|
||||
let (acknowledged, progressed) = self.process_up_inner(sequence, body)?;
|
||||
if self.automatic_carrier && !progressed && !self.is_carrier_committed() {
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
Ok(acknowledged)
|
||||
}
|
||||
|
||||
/// Applies one WebSocket uplink batch and reports actual carrier progress.
|
||||
@@ -182,6 +185,9 @@ impl WebSession {
|
||||
|| state.closing_streams.contains_key(&value.stream_id);
|
||||
match value.frame_type {
|
||||
FrameType::Open => {
|
||||
let Some(stream) = next_stream_identity(state, value.stream_id) else {
|
||||
return false;
|
||||
};
|
||||
let peer_port = match reserved_open.take() {
|
||||
Some((reserved_stream_id, peer_port))
|
||||
if reserved_stream_id == value.stream_id =>
|
||||
@@ -208,9 +214,6 @@ impl WebSession {
|
||||
peer_port
|
||||
}
|
||||
};
|
||||
let Some(stream) = next_stream_identity(state, value.stream_id) else {
|
||||
return false;
|
||||
};
|
||||
state.streams.insert(
|
||||
value.stream_id,
|
||||
StreamState {
|
||||
@@ -420,6 +423,10 @@ mod tests {
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
fn session() -> Arc<WebSession> {
|
||||
session_with_automatic(false)
|
||||
}
|
||||
|
||||
fn session_with_automatic(automatic: bool) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
@@ -447,9 +454,13 @@ mod tests {
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
if automatic {
|
||||
crate::web::manager::CarrierClientClass::Bridge
|
||||
} else {
|
||||
crate::web::manager::CarrierClientClass::Legacy
|
||||
},
|
||||
None,
|
||||
false,
|
||||
automatic,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
@@ -515,4 +526,14 @@ mod tests {
|
||||
assert_eq!(session.process_up(2, &body), Err(ManagerError::Protocol));
|
||||
assert!(session.state.lock().closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_uplink_does_not_ack_a_batch_without_real_progress() {
|
||||
let session = session_with_automatic(true);
|
||||
let body = frame::encode(FrameType::Pong, 0, &[]);
|
||||
|
||||
assert_eq!(session.process_up(1, &body), Err(ManagerError::Backpressure));
|
||||
assert!(!session.is_carrier_committed());
|
||||
assert!(!session.state.lock().closed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user