Proxy Shared User Drafts

This commit is contained in:
Alexey
2026-09-14 20:19:48 +03:00
parent b37f1ebdeb
commit 0ac236955a
59 changed files with 1485 additions and 401 deletions
+1
View File
@@ -123,6 +123,7 @@ fn runtime_config_with_carriers_and_deadlines(
carriers: Arc::clone(&carriers),
carrier_negotiation_deadlines_secs,
capability,
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 4,
max_streams: 16,
+16 -12
View File
@@ -89,11 +89,6 @@ impl WebProcessRuntime {
.record_rejection(WebRejectionReason::ConfigDisabled);
return Err(ManagerError::Closed);
}
if !generation.proxy_shared.is_user_enabled(&profile.user) {
self.telemetry
.record_rejection(WebRejectionReason::UserDisabled);
return Err(ManagerError::Closed);
}
let _operator_admission = self.try_operator_admission()?;
let now = Instant::now();
let mut state = self.state.lock();
@@ -146,12 +141,25 @@ impl WebProcessRuntime {
.record_rejection(WebRejectionReason::BootstrapCapacity);
return Err(ManagerError::Limit);
};
let Some(mut user_publication) = generation
.proxy_shared
.claim_authenticated_user(&profile.user, profile.credential_id)
else {
self.telemetry
.record_rejection(WebRejectionReason::UserDisabled);
return Err(ManagerError::Closed);
};
let Some(user_registration) = user_publication.take_registration() else {
return Err(ManagerError::Closed);
};
let trace_session_id = self.trace.next_session_id();
let bridge_diagnostics_enabled = config.web.debug.bridge_diagnostics_enabled();
let (user_agent, user_agent_id) = bounded_user_agent(user_agent);
let issued_profile = Arc::clone(&profile);
state.bootstraps.insert(
hash,
Bootstrap {
user_registration,
expires_at: now + Duration::from_secs(config.web.timeouts.bootstrap_lifetime_secs),
issued_at: now,
issuance_ip: client_ip,
@@ -186,11 +194,7 @@ impl WebProcessRuntime {
},
);
*state.bootstraps_per_ip.entry(client_ip).or_insert(0) += 1;
let profile = state
.bootstraps
.get(&hash)
.map(|entry| Arc::clone(&entry.profile))
.ok_or(ManagerError::Closed)?;
user_publication.commit();
drop(state);
if recovery {
self.telemetry
@@ -202,7 +206,7 @@ impl WebProcessRuntime {
Some(client_ip),
crate::web::trace::TraceIdentity::from_optional_profile(
Some(trace_session_id),
&profile,
&issued_profile,
),
crate::web::trace::TraceLifecycleEvent::BridgeIssued,
None,
@@ -216,7 +220,7 @@ impl WebProcessRuntime {
self.trace.record_profile_lifecycle(
client_ip,
Some(trace_session_id),
&profile,
&issued_profile,
crate::web::trace::TraceLifecycleEvent::BridgeIssued,
None,
None,
+4 -1
View File
@@ -30,14 +30,17 @@ pub(crate) struct WebShutdownDrain {
}
impl WebProcessRuntime {
/// Applies learning policy before publishing one new runtime generation.
/// Applies issuance and learning policy before publishing one new generation.
pub(crate) fn activate_generation(
&self,
generation: Arc<RuntimeGeneration>,
) -> Arc<RuntimeGeneration> {
let config = generation.config();
let (replaced, detached) = {
// Manager state precedes learning in the request-path lock order.
let mut state = self.state.lock();
let mut learning = self.learning.lock();
state.apply_issuance_policy(generation.id, config.web.enabled);
let outcome = learning.apply_policy(
Instant::now(),
generation.id,
+15 -1
View File
@@ -69,7 +69,10 @@ impl WebProcessRuntime {
let Some(entry) = state.bootstraps.get(&bootstrap_hash) else {
return Err(ManagerError::Authentication);
};
if entry.profile.host != host || now > entry.expires_at {
if entry.profile.host != host
|| now > entry.expires_at
|| entry.user_registration.is_cancelled()
{
return Err(ManagerError::Authentication);
}
if entry.used {
@@ -301,6 +304,15 @@ impl WebProcessRuntime {
return Err(ManagerError::Protocol);
};
let _operator_admission = self.try_operator_admission()?;
let Some(mut user_publication) = generation
.proxy_shared
.claim_authenticated_user(&profile.user, profile.credential_id)
else {
return Err(ManagerError::Closed);
};
let Some(user_registration) = user_publication.take_registration() else {
return Err(ManagerError::Closed);
};
if !admit_initial(self, &mut state, now, client_ip, profile_key, &profile) {
return Err(ManagerError::Limit);
}
@@ -338,6 +350,7 @@ impl WebProcessRuntime {
recovery,
self.limits.clone(),
issued_timeouts.clone(),
Some(user_registration),
);
state.sessions.insert(session_hash, Arc::clone(&session));
*state.sessions_per_ip.entry(client_ip).or_insert(0) += 1;
@@ -402,6 +415,7 @@ impl WebProcessRuntime {
user_agent_id,
},
);
user_publication.commit();
drop(state);
self.telemetry
.record_carrier_selection(carrier, learning_disposition);
@@ -43,14 +43,24 @@ impl WebProcessRuntime {
if !valid
|| state.closed
|| !state.issuance_enabled
|| !generation
.proxy_shared
.is_user_enabled(&replacement.profile.user)
{
drop(state);
self.cancel_replacement(bootstrap_hash, &replacement.old_session);
return Err(ManagerError::Closed);
}
let Some(mut user_publication) = generation.proxy_shared.claim_authenticated_user(
&replacement.profile.user,
replacement.profile.credential_id,
) else {
drop(state);
self.cancel_replacement(bootstrap_hash, &replacement.old_session);
return Err(ManagerError::Closed);
};
let Some(user_registration) = user_publication.take_registration() else {
drop(state);
self.cancel_replacement(bootstrap_hash, &replacement.old_session);
return Err(ManagerError::Closed);
};
let Some((session_token, session_hash)) = new_unique_token(&generation, &state) else {
self.record_limit_hit();
self.telemetry
@@ -86,6 +96,7 @@ impl WebProcessRuntime {
replacement.recovery,
self.limits.clone(),
replacement.old_session.timeouts().clone(),
Some(user_registration),
);
let Some(supersede) = replacement.old_session.prepare_carrier_supersede() else {
drop(state);
@@ -146,6 +157,7 @@ impl WebProcessRuntime {
index.bootstrap_hash = bootstrap_hash;
index.attempt = replacement.attempt;
}
user_publication.commit();
let identity = session.trace_identity();
let old_identity = replacement.old_session.trace_identity();
drop(state);
+4
View File
@@ -10,6 +10,7 @@ use zeroize::Zeroizing;
use super::{CarrierRequest, ProfileKey, TOKEN_BYTES, TokenHash};
use crate::config::{WebCarrier, WebRuntimeConfig, WebRuntimeProfile, WebTimeoutsConfig};
use crate::maestro::generation::RuntimeGeneration;
use crate::proxy::user_admission::UserSessionRegistration;
use crate::web::session::WebSession;
use crate::web::telemetry::WebCarrierSelectionDisposition;
@@ -34,6 +35,8 @@ impl CarrierChainPhase {
/// One issued bootstrap and optional idempotent session-creation replay state.
pub(super) struct Bootstrap {
/// User authority ownership retained for the credential lifetime.
pub(super) user_registration: UserSessionRegistration,
/// Credential and replay-state expiry deadline.
pub(super) expires_at: Instant,
/// Stable ordering point used for bounded eviction.
@@ -273,6 +276,7 @@ pub(super) fn matching_profile(
profile.host == expected.host
&& profile.public_addr == expected.public_addr
&& profile.user == expected.user
&& profile.credential_id == expected.credential_id
&& profile.secret_mode == expected.secret_mode
&& profile.carrier == expected.carrier
&& profile.carrier_negotiation_enabled == expected.carrier_negotiation_enabled
+9 -1
View File
@@ -17,6 +17,7 @@ use crate::web::frame::{self, FrameType};
use crate::web::manager::{
CarrierClientClass, CarrierLearningContext, ProfileKey, TokenHash, WebProcessRuntime,
};
use crate::proxy::user_admission::UserSessionRegistration;
// Backend tasks own generation admission and authenticated MTProxy relay lifetimes.
mod backend;
@@ -213,6 +214,7 @@ pub(crate) struct WebSession {
created_at: Instant,
limits: WebLimitsConfig,
timeouts: WebTimeoutsConfig,
_user_registration: Option<UserSessionRegistration>,
state: Mutex<SessionState>,
carrier_health_publication: AtomicU8,
close_complete: AtomicBool,
@@ -257,8 +259,13 @@ impl WebSession {
recovery: bool,
limits: WebLimitsConfig,
timeouts: WebTimeoutsConfig,
user_registration: Option<UserSessionRegistration>,
) -> Arc<Self> {
let created_at = Instant::now();
let cancel = user_registration
.as_ref()
.map(UserSessionRegistration::token)
.unwrap_or_default();
let mut carrier_lanes = HashMap::new();
let mut next_lane_instance = 1;
if selected_carrier == WebCarrier::HttpsLanes {
@@ -283,6 +290,7 @@ impl WebSession {
created_at,
limits,
timeouts,
_user_registration: user_registration,
state: Mutex::new(SessionState {
streams: HashMap::new(),
closing_streams: HashMap::new(),
@@ -328,7 +336,7 @@ impl WebSession {
close_notify: Notify::new(),
down_notify: Arc::new(Notify::new()),
lane_open_notify: Arc::new(Notify::new()),
cancel: CancellationToken::new(),
cancel,
tasks_live: AtomicUsize::new(0),
tasks_done: Arc::new(Notify::new()),
resident: Arc::new(resident::ResidentCounters::default()),
+2
View File
@@ -75,6 +75,7 @@ fn test_runtime_with_dc(
carriers: Arc::from([carrier]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [7; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 4,
max_streams: 16,
@@ -134,6 +135,7 @@ fn test_runtime_with_dc(
false,
limits,
timeouts,
None,
);
TestRuntime {
session,
+2
View File
@@ -24,6 +24,7 @@ fn session() -> (Arc<WebSession>, Arc<WebProcessRuntime>) {
carriers: Arc::from([WebCarrier::Https]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [0; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 1,
max_streams: 1,
@@ -50,6 +51,7 @@ fn session() -> (Arc<WebSession>, Arc<WebProcessRuntime>) {
false,
WebLimitsConfig::default(),
timeouts,
None,
);
(session, manager)
}
+2
View File
@@ -39,6 +39,7 @@ fn new_session_with_automatic(
carriers: Arc::from([WebCarrier::HttpsLanes]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [0; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 1,
max_streams: 2,
@@ -65,6 +66,7 @@ fn new_session_with_automatic(
false,
limits,
WebTimeoutsConfig::default(),
None,
)
}
+19 -1
View File
@@ -25,6 +25,8 @@ pub(crate) enum SessionCloseReason {
WebSocketEnded,
/// An authenticated control-plane request selected this session.
ApiClose,
/// Process user authority revoked or replaced the authenticated credential.
UserDisabled,
/// A graceful operator drain reached its force-close deadline.
OperatorForce,
/// Terminal process shutdown closed all remaining sessions.
@@ -33,7 +35,7 @@ pub(crate) enum SessionCloseReason {
impl SessionCloseReason {
/// Complete fixed reason set in stable API and metric order.
pub(crate) const ALL: [Self; 11] = [
pub(crate) const ALL: [Self; 12] = [
Self::ClientDelete,
Self::BridgeRecovery,
Self::PeerIdle,
@@ -43,6 +45,7 @@ impl SessionCloseReason {
Self::Backpressure,
Self::WebSocketEnded,
Self::ApiClose,
Self::UserDisabled,
Self::OperatorForce,
Self::RuntimeShutdown,
];
@@ -59,6 +62,7 @@ impl SessionCloseReason {
Self::Backpressure => "backpressure",
Self::WebSocketEnded => "websocket_ended",
Self::ApiClose => "api_close",
Self::UserDisabled => "user_disabled",
Self::OperatorForce => "operator_force",
Self::RuntimeShutdown => "runtime_shutdown",
}
@@ -217,6 +221,20 @@ impl WebSession {
/// Atomically closes a session only when reconnect grace is still due.
pub(crate) fn close_if_due(&self, now: Instant) -> bool {
if self.cancel.is_cancelled() {
let released = {
let mut state = self.state.lock();
if state.closed || state.close_requested.is_some() {
None
} else {
Some(self.release_on_close_locked(&mut state, SessionCloseReason::UserDisabled))
}
};
if let Some(released) = released {
self.finish_close(released);
return true;
}
}
let healthy = {
let mut state = self.state.lock();
self.carrier_health_ready_locked(&mut state, now)
+2
View File
@@ -320,6 +320,7 @@ mod tests {
carriers: Arc::from([carrier]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [0; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 1,
max_streams: 1,
@@ -342,6 +343,7 @@ mod tests {
false,
WebLimitsConfig::default(),
WebTimeoutsConfig::default(),
None,
)
}
+2
View File
@@ -22,6 +22,7 @@ fn session_with_automatic(automatic: bool) -> Arc<WebSession> {
carriers: Arc::from([WebCarrier::Https]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [0; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 1,
max_streams: 1,
@@ -48,6 +49,7 @@ fn session_with_automatic(automatic: bool) -> Arc<WebSession> {
false,
WebLimitsConfig::default(),
WebTimeoutsConfig::default(),
None,
)
}
+2
View File
@@ -39,6 +39,7 @@ fn runtime(admission: bool) -> TestRuntime {
carriers: Arc::from([WebCarrier::WebsocketLanes]),
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
capability: [7; 32],
credential_id: [0; 16],
key_fingerprint: "0000000000000000".to_string(),
max_sessions: 2,
max_streams: 1,
@@ -75,6 +76,7 @@ fn runtime(admission: bool) -> TestRuntime {
false,
limits,
timeouts,
None,
);
TestRuntime {
session,