mirror of
https://github.com/telemt/telemt.git
synced 2026-09-16 23:44:11 +03:00
WEB Debug + Trace
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
+34
-223
@@ -1,34 +1,27 @@
|
||||
use std::future::Future;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
use tokio::sync::{Notify, OwnedSemaphorePermit, Semaphore};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::config::{WebCarrier, WebLimitsConfig, WebRuntimeProfile};
|
||||
use crate::config::{WebCarrier, WebLimitsConfig};
|
||||
use crate::maestro::generation::RuntimeGeneration;
|
||||
use crate::web::frame;
|
||||
use crate::web::session::WebSession;
|
||||
use crate::web::trace::WebTraceStore;
|
||||
|
||||
// Credential maps, quotas, and token-bucket helpers remain private to the manager.
|
||||
mod state;
|
||||
// Bootstrap credentials and idempotent session creation are isolated from queue accounting.
|
||||
mod credentials;
|
||||
// Stream admission and synthetic tuple ownership are process-scoped.
|
||||
mod admission;
|
||||
// Shutdown and expiry work remain outside request-path coordination.
|
||||
mod lifecycle;
|
||||
use state::{
|
||||
Bootstrap, ManagerState, allow_rate, control_item_reserve, decrement_map,
|
||||
evict_oldest_unused_bootstrap, matching_profile, new_unique_token, profile_key,
|
||||
remove_expired_locked,
|
||||
};
|
||||
use state::{ManagerState, control_item_reserve};
|
||||
|
||||
const TOKEN_BYTES: usize = 32;
|
||||
const CLEANUP_INTERVAL: Duration = Duration::from_secs(1);
|
||||
@@ -63,9 +56,18 @@ pub(crate) struct CreateResult {
|
||||
pub(crate) carrier: WebCarrier,
|
||||
}
|
||||
|
||||
/// Successful bridge bootstrap issuance result.
|
||||
pub(crate) struct BootstrapResult {
|
||||
/// Opaque one-use bootstrap credential.
|
||||
pub(crate) token: String,
|
||||
/// Process-unique non-secret trace identifier.
|
||||
pub(crate) trace_session_id: u64,
|
||||
}
|
||||
|
||||
/// Process-owned bounded WEB credential, session, and memory coordinator.
|
||||
pub(crate) struct WebProcessRuntime {
|
||||
active_runtime: Arc<ArcSwap<RuntimeGeneration>>,
|
||||
trace: Arc<WebTraceStore>,
|
||||
limits: WebLimitsConfig,
|
||||
state: Mutex<ManagerState>,
|
||||
http_connections: Arc<Semaphore>,
|
||||
@@ -89,10 +91,22 @@ pub(crate) struct WebProcessRuntime {
|
||||
|
||||
impl WebProcessRuntime {
|
||||
/// Starts one process-scoped manager using immutable allocation ceilings.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn start(active_runtime: Arc<ArcSwap<RuntimeGeneration>>) -> Arc<Self> {
|
||||
let config = active_runtime.load().config();
|
||||
let trace = WebTraceStore::new(config.web.debug.clone(), &config.web.limits);
|
||||
Self::start_with_trace(active_runtime, trace)
|
||||
}
|
||||
|
||||
/// Starts one process-scoped manager with a shared API-visible trace store.
|
||||
pub(crate) fn start_with_trace(
|
||||
active_runtime: Arc<ArcSwap<RuntimeGeneration>>,
|
||||
trace: Arc<WebTraceStore>,
|
||||
) -> Arc<Self> {
|
||||
let limits = active_runtime.load().config().web.limits.clone();
|
||||
let runtime = Arc::new(Self {
|
||||
active_runtime,
|
||||
trace,
|
||||
http_connections: Arc::new(Semaphore::new(limits.max_http_connections)),
|
||||
http_handlers: Arc::new(Semaphore::new(limits.max_http_handlers)),
|
||||
lane_polls: Arc::new(Semaphore::new((limits.max_http_handlers / 2).max(1))),
|
||||
@@ -125,6 +139,8 @@ impl WebProcessRuntime {
|
||||
let Some(runtime) = weak.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let policy = runtime.active_generation().config().web.debug.clone();
|
||||
runtime.trace.apply_policy(&policy);
|
||||
runtime.cleanup();
|
||||
}
|
||||
}
|
||||
@@ -138,6 +154,11 @@ impl WebProcessRuntime {
|
||||
self.active_runtime.load_full()
|
||||
}
|
||||
|
||||
/// Returns the process-owned WEB debug trace store.
|
||||
pub(crate) fn trace(&self) -> &Arc<WebTraceStore> {
|
||||
&self.trace
|
||||
}
|
||||
|
||||
/// Reserves one accepted HTTP connection.
|
||||
pub(crate) fn try_http_connection(&self) -> Option<OwnedSemaphorePermit> {
|
||||
let permit = Arc::clone(&self.http_connections).try_acquire_owned().ok();
|
||||
@@ -211,216 +232,6 @@ impl WebProcessRuntime {
|
||||
Some((reader, body))
|
||||
}
|
||||
|
||||
/// Issues a one-use bootstrap credential for an active compatible profile.
|
||||
pub(crate) fn issue_bootstrap(
|
||||
&self,
|
||||
profile: Arc<WebRuntimeProfile>,
|
||||
client_ip: IpAddr,
|
||||
) -> std::result::Result<String, ManagerError> {
|
||||
let generation = self.active_generation();
|
||||
let config = generation.config();
|
||||
let profile = config
|
||||
.web
|
||||
.runtime
|
||||
.as_ref()
|
||||
.and_then(|runtime| matching_profile(runtime, &profile))
|
||||
.ok_or(ManagerError::Authentication)?;
|
||||
if !config.web.enabled || !generation.proxy_shared.is_user_enabled(&profile.user) {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let now = Instant::now();
|
||||
let mut state = self.state.lock();
|
||||
remove_expired_locked(&mut state, now);
|
||||
if state.closed
|
||||
|| state
|
||||
.bootstraps_per_ip
|
||||
.get(&client_ip)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
>= self.limits.max_bootstraps_per_ip
|
||||
|| !allow_rate(
|
||||
&mut state.bootstrap_rate,
|
||||
now,
|
||||
self.limits.new_bootstraps_per_minute,
|
||||
self.limits.new_bootstraps_burst,
|
||||
)
|
||||
{
|
||||
self.limit_hits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
if state.bootstraps.len() >= self.limits.max_bootstraps_global
|
||||
&& !evict_oldest_unused_bootstrap(&mut state)
|
||||
{
|
||||
self.limit_hits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some((token, hash)) = new_unique_token(&generation, &state) else {
|
||||
self.limit_hits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
state.bootstraps.insert(
|
||||
hash,
|
||||
Bootstrap {
|
||||
expires_at: now + Duration::from_secs(config.web.timeouts.bootstrap_lifetime_secs),
|
||||
issued_at: now,
|
||||
issuance_ip: client_ip,
|
||||
profile,
|
||||
body_digest: [0; TOKEN_BYTES],
|
||||
session_token: Zeroizing::new(String::new()),
|
||||
session: None,
|
||||
used: false,
|
||||
},
|
||||
);
|
||||
*state.bootstraps_per_ip.entry(client_ip).or_insert(0) += 1;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// Checks whether a bootstrap token is live before reading a request body.
|
||||
pub(crate) fn has_bootstrap(&self, hash: TokenHash, host: &str) -> bool {
|
||||
let now = Instant::now();
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.bootstraps
|
||||
.get(&hash)
|
||||
.is_some_and(|entry| entry.profile.host == host && now <= entry.expires_at)
|
||||
}
|
||||
|
||||
/// Creates a session exactly once or replays the original successful result.
|
||||
pub(crate) fn create_session(
|
||||
self: &Arc<Self>,
|
||||
bootstrap_hash: TokenHash,
|
||||
host: &str,
|
||||
client_ip: IpAddr,
|
||||
body: &[u8],
|
||||
) -> std::result::Result<CreateResult, ManagerError> {
|
||||
if !frame::validate_hello(body, &self.limits) {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let body_digest: TokenHash = Sha256::digest(body).into();
|
||||
let generation = self.active_generation();
|
||||
let config = generation.config();
|
||||
let now = Instant::now();
|
||||
let mut state = self.state.lock();
|
||||
remove_expired_locked(&mut state, now);
|
||||
let Some(entry) = state.bootstraps.get(&bootstrap_hash) else {
|
||||
return Err(ManagerError::Authentication);
|
||||
};
|
||||
if entry.profile.host != host || now > entry.expires_at {
|
||||
return Err(ManagerError::Authentication);
|
||||
}
|
||||
if entry.used {
|
||||
let digest_matches = bool::from(entry.body_digest.ct_eq(&body_digest));
|
||||
if !digest_matches {
|
||||
return Err(ManagerError::Authentication);
|
||||
}
|
||||
let session = entry.session.as_ref().ok_or(ManagerError::Authentication)?;
|
||||
return Ok(CreateResult {
|
||||
token: entry.session_token.as_str().to_owned(),
|
||||
carrier: session.carrier(),
|
||||
});
|
||||
}
|
||||
if state.closed || !config.web.enabled {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let profile = config
|
||||
.web
|
||||
.runtime
|
||||
.as_ref()
|
||||
.and_then(|runtime| matching_profile(runtime, &entry.profile))
|
||||
.filter(|profile| generation.proxy_shared.is_user_enabled(&profile.user))
|
||||
.ok_or(ManagerError::Authentication)?;
|
||||
let profile_key = profile_key(&profile);
|
||||
if state.sessions.len() >= self.limits.max_sessions_global
|
||||
|| state.sessions_per_ip.get(&client_ip).copied().unwrap_or(0)
|
||||
>= self.limits.max_sessions_per_ip
|
||||
|| state
|
||||
.sessions_per_profile
|
||||
.get(&profile_key)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
>= profile.max_sessions
|
||||
|| !allow_rate(
|
||||
&mut state.session_rate,
|
||||
now,
|
||||
self.limits.new_sessions_per_minute,
|
||||
self.limits.new_sessions_burst,
|
||||
)
|
||||
{
|
||||
self.limit_hits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some((session_token, session_hash)) = new_unique_token(&generation, &state) else {
|
||||
self.limit_hits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
let session = WebSession::new(
|
||||
Arc::downgrade(self),
|
||||
session_hash,
|
||||
client_ip,
|
||||
profile,
|
||||
profile_key,
|
||||
self.limits.clone(),
|
||||
config.web.timeouts.clone(),
|
||||
);
|
||||
state.sessions.insert(session_hash, Arc::clone(&session));
|
||||
*state.sessions_per_ip.entry(client_ip).or_insert(0) += 1;
|
||||
*state.sessions_per_profile.entry(profile_key).or_insert(0) += 1;
|
||||
let entry = state
|
||||
.bootstraps
|
||||
.get_mut(&bootstrap_hash)
|
||||
.ok_or(ManagerError::Authentication)?;
|
||||
entry.used = true;
|
||||
entry.body_digest = body_digest;
|
||||
entry.session_token = Zeroizing::new(session_token.clone());
|
||||
entry.session = Some(Arc::clone(&session));
|
||||
let issuance_ip = entry.issuance_ip;
|
||||
decrement_map(&mut state.bootstraps_per_ip, &issuance_ip);
|
||||
self.sessions_created.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(CreateResult {
|
||||
token: session_token,
|
||||
carrier: session.carrier(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves an authenticated session token.
|
||||
pub(crate) fn get_session(
|
||||
&self,
|
||||
hash: TokenHash,
|
||||
host: &str,
|
||||
) -> std::result::Result<Arc<WebSession>, ManagerError> {
|
||||
self.state
|
||||
.lock()
|
||||
.sessions
|
||||
.get(&hash)
|
||||
.cloned()
|
||||
.filter(|session| session.matches_host(host))
|
||||
.ok_or(ManagerError::Authentication)
|
||||
}
|
||||
|
||||
/// Closes a live token and accepts bounded tombstone retries.
|
||||
pub(crate) fn close_token(
|
||||
&self,
|
||||
hash: TokenHash,
|
||||
host: &str,
|
||||
) -> std::result::Result<(), ManagerError> {
|
||||
let state = self.state.lock();
|
||||
let session = state
|
||||
.sessions
|
||||
.get(&hash)
|
||||
.filter(|session| session.matches_host(host))
|
||||
.cloned();
|
||||
let closed = state
|
||||
.closed_tokens
|
||||
.get(&hash)
|
||||
.is_some_and(|closed| closed.host == host);
|
||||
drop(state);
|
||||
if let Some(session) = session {
|
||||
session.close();
|
||||
return Ok(());
|
||||
}
|
||||
closed.then_some(()).ok_or(ManagerError::Authentication)
|
||||
}
|
||||
|
||||
/// Reserves bounded process-wide queue capacity for data or control traffic.
|
||||
pub(crate) fn try_reserve_pending(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user