diagnostics, recconections fix and bufferbloat fixes
This commit is contained in:
+39
-13
@@ -26,25 +26,51 @@ pub struct NetworkConfig {
|
||||
pub udp_meta_light: usize,
|
||||
}
|
||||
|
||||
/// MTU-aware defaults.
|
||||
///
|
||||
/// TCP socket buffers scale with the MTU so that the initial smoltcp window
|
||||
/// always fits at least `WINDOW_SEGMENTS` back-to-back segments. Channel
|
||||
/// capacity is sized to hold at most `CHANNEL_PACKETS` MTU-sized packets.
|
||||
impl NetworkConfig {
|
||||
pub fn new(system_mtu: usize) -> Self {
|
||||
// Keep a minimum useful MTU even if the caller supplies something tiny.
|
||||
let mtu = system_mtu.max(576);
|
||||
|
||||
// Minimum number of segments that must fit in a full TCP RX/TX buffer.
|
||||
const BULK_WINDOW_SEGMENTS: usize = 128;
|
||||
const LIGHT_WINDOW_SEGMENTS: usize = 32;
|
||||
|
||||
// How many MTU-sized packets the Tokio mpsc channel should hold.
|
||||
// At MTU 1450 and 128 slots: ~185 KB per channel — enough to absorb
|
||||
// ~15 ms of jitter at 100 Mbps without spawning backpressure tasks.
|
||||
const CHANNEL_PACKETS: usize = 128;
|
||||
|
||||
// Payload bytes per segment (no IP/TCP headers in the smoltcp buffer).
|
||||
let seg = mtu.saturating_sub(40).max(512); // subtract typical IP+TCP overhead
|
||||
|
||||
// Round up to the nearest 4 KB for alignment.
|
||||
let round = |n: usize| ((n + 4095) / 4096) * 4096;
|
||||
|
||||
let tcp_heavy = round(seg * BULK_WINDOW_SEGMENTS);
|
||||
let tcp_light = round(seg * LIGHT_WINDOW_SEGMENTS);
|
||||
|
||||
Self {
|
||||
mtu: system_mtu,
|
||||
connection_buf_size: 128 * 1024, // Уменьшили с 256KB
|
||||
tcp_buffer_size: 128 * 1024, // Уменьшили с 256KB
|
||||
udp_buffer_size: 64 * 1024, // Уменьшили со 128KB
|
||||
tcp_chunk_size: 1024 * 64,
|
||||
mtu,
|
||||
connection_buf_size: tcp_heavy,
|
||||
tcp_buffer_size: tcp_heavy,
|
||||
udp_buffer_size: tcp_light,
|
||||
// Read chunks up to one smoltcp frame payload; larger values just
|
||||
// add latency without improving throughput.
|
||||
tcp_chunk_size: 64 * 1024,
|
||||
|
||||
channel_capacity: 16,
|
||||
channel_capacity: CHANNEL_PACKETS,
|
||||
|
||||
// Окна smoltcp (уменьшаем, чтобы не создавать огромные очереди)
|
||||
tcp_rx_heavy: 128 * 1024,
|
||||
tcp_tx_heavy: 128 * 1024,
|
||||
tcp_rx_heavy: tcp_heavy,
|
||||
tcp_tx_heavy: tcp_heavy,
|
||||
tcp_rx_light: tcp_light,
|
||||
tcp_tx_light: tcp_light,
|
||||
|
||||
tcp_rx_light: 32 * 1024, // Уменьшили с 64KB
|
||||
tcp_tx_light: 32 * 1024,
|
||||
|
||||
udp_buf_heavy: 128 * 1024,
|
||||
udp_buf_heavy: tcp_heavy,
|
||||
udp_meta_heavy: 512,
|
||||
udp_buf_light: 16 * 1024,
|
||||
udp_meta_light: 32,
|
||||
|
||||
@@ -8,9 +8,10 @@ use crate::{
|
||||
handler::{RemoteOpener, StreamHandler},
|
||||
muxer::{MuxMessage, Muxer},
|
||||
},
|
||||
NetworkConfig, FALLBACK_CONNECT_TIMEOUT, LEG_RECONNECT_DELAY, LEG_STAGGER_DELAY,
|
||||
MAX_TUNNEL_LEGS, SECURE_HANDSHAKE_TIMEOUT, STEALTH_FALLBACK_HOST, TLS_HELLO_TIMEOUT,
|
||||
TOPOLOGY_PRINT_INTERVAL,
|
||||
DNS_LOOKUP_TIMEOUT, FALLBACK_CONNECT_TIMEOUT, LEG_RECONNECT_DELAY, LEG_STAGGER_DELAY,
|
||||
MAX_TUNNEL_LEGS, NETWORK_WATCHER_INTERVAL, SECURE_HANDSHAKE_TIMEOUT,
|
||||
SESSION_CLEANUP_DELAY, STEALTH_FALLBACK_HOST, STEALTH_FALLBACK_SNI, TLS_HELLO_TIMEOUT,
|
||||
TOPOLOGY_PRINT_INTERVAL, NetworkConfig,
|
||||
},
|
||||
nrxp::{Codec, Frame, FrameType, TlsBridge},
|
||||
rawcast::{LocalProtocol, RawCastAdapter, RawCastFrame},
|
||||
@@ -126,7 +127,7 @@ impl ClientHandler {
|
||||
let mut conn = Connection::new(stream);
|
||||
let mut session_keys = SessionKeys::new(true);
|
||||
let ch =
|
||||
TlsBridge::wrap_client_hello(&BrowserProfile::CHROME_131, "ubuntu.com", &session_keys);
|
||||
TlsBridge::wrap_client_hello(&BrowserProfile::CHROME_131, STEALTH_FALLBACK_SNI, &session_keys);
|
||||
|
||||
conn.outbound
|
||||
.write_all(&ch)
|
||||
@@ -141,7 +142,7 @@ impl ClientHandler {
|
||||
}
|
||||
Ok(None) => {
|
||||
let res = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
TLS_HELLO_TIMEOUT,
|
||||
conn.inbound.read_buf(&mut conn.read_buf),
|
||||
)
|
||||
.await;
|
||||
@@ -214,7 +215,7 @@ impl ClientHandler {
|
||||
let leg_name = format!("TCP-Leg-{}", leg_id);
|
||||
|
||||
let addrs_future = tokio::net::lookup_host(remote_proxy_addr);
|
||||
let mut addrs = tokio::time::timeout(std::time::Duration::from_secs(3), addrs_future)
|
||||
let mut addrs = tokio::time::timeout(DNS_LOOKUP_TIMEOUT, addrs_future)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::new(ERR_INFRA_TIMEOUT, "Сервер недоступен", "DNS Lookup Timeout")
|
||||
@@ -230,7 +231,7 @@ impl ClientHandler {
|
||||
})?;
|
||||
|
||||
let stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
FALLBACK_CONNECT_TIMEOUT,
|
||||
tokio::net::TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
@@ -250,7 +251,6 @@ impl ClientHandler {
|
||||
let (control_tx, control_rx) = mpsc::channel::<MuxMessage>(cap);
|
||||
let (data_tx, data_rx) = mpsc::channel::<MuxMessage>(cap);
|
||||
|
||||
let control_tx_clone = control_tx.clone();
|
||||
muxer.add_leg(leg_id, control_tx, data_tx);
|
||||
|
||||
let handler = Arc::new(StreamHandler::new(muxer.clone(), None));
|
||||
@@ -272,7 +272,9 @@ impl ClientHandler {
|
||||
};
|
||||
|
||||
let run_result = engine.run().await;
|
||||
muxer.remove_leg(leg_id, &control_tx_clone);
|
||||
// Use force_remove because the engine may have re-registered the leg internally
|
||||
// (via reconnect + add_leg), making control_tx_clone stale for same_channel comparison.
|
||||
muxer.force_remove_leg(leg_id);
|
||||
|
||||
run_result?;
|
||||
Err(AppError::new(
|
||||
@@ -286,7 +288,7 @@ impl ClientHandler {
|
||||
remote_proxy_addr: &str,
|
||||
mut rx_from_engine: mpsc::Receiver<RawCastFrame>,
|
||||
tx_to_engine: mpsc::Sender<RawCastFrame>,
|
||||
) -> Result<(), AppError> {
|
||||
) -> Result<Arc<Muxer>, AppError> {
|
||||
let session_id = SessionManager::generate_id();
|
||||
let muxer = Arc::new(Muxer::new(true, session_id.clone()));
|
||||
let registry: Arc<DashMap<u32, (u64, Ipv4Addr, u16, LocalProtocol)>> =
|
||||
@@ -297,7 +299,7 @@ impl ClientHandler {
|
||||
let watcher_muxer = muxer.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut last_ip = Self::get_local_ip();
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
||||
let mut interval = tokio::time::interval(NETWORK_WATCHER_INTERVAL);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let current_ip = Self::get_local_ip();
|
||||
@@ -321,10 +323,31 @@ impl ClientHandler {
|
||||
let sid = session_id.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(LEG_STAGGER_DELAY * id).await;
|
||||
let mut attempt: u32 = 0;
|
||||
loop {
|
||||
if let Err(e) = Self::establish_leg(&addr, id, m.clone(), &sid).await {
|
||||
attempt += 1;
|
||||
error!("Leg {} disconnected: {}. Reconnecting in 2s...", id, e);
|
||||
let rtt = crate::net::GLOBAL_MIN_RTT.load(std::sync::atomic::Ordering::Relaxed);
|
||||
crate::net::diagnostics::DIAG_COUNTERS
|
||||
.leg_disconnects
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
crate::net::diagnostics::send_diag_event(
|
||||
crate::net::diagnostics::DiagnosticsEvent::LegDisconnected {
|
||||
leg_id: id,
|
||||
rtt_ms: rtt,
|
||||
reason: e.to_string(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(LEG_RECONNECT_DELAY).await;
|
||||
crate::net::diagnostics::send_diag_event(
|
||||
crate::net::diagnostics::DiagnosticsEvent::LegReconnecting {
|
||||
leg_id: id,
|
||||
attempt,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
attempt = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -431,15 +454,11 @@ impl ClientHandler {
|
||||
});
|
||||
}
|
||||
FrameType::Data | FrameType::UdpData => {
|
||||
if let Some(up_tx) = local_to_upload_tx.get(&local_socket_id) {
|
||||
if let Err(mpsc::error::TrySendError::Full(_)) =
|
||||
up_tx.try_send(payload)
|
||||
{
|
||||
netrunner_logger::trace!(
|
||||
local_socket_id,
|
||||
"⚠️ Upload stream buffer full, dropping frame (TCP will retransmit)"
|
||||
);
|
||||
}
|
||||
if let Some(up_tx) = local_to_upload_tx.get(&local_socket_id).map(|r| r.value().clone()) {
|
||||
// .send().await blocks the upload task when the muxer leg
|
||||
// is saturated, creating back-pressure back to smoltcp
|
||||
// (no drops → no unnecessary retransmits → lower jitter).
|
||||
let _ = up_tx.send(payload).await;
|
||||
}
|
||||
}
|
||||
FrameType::Close => {
|
||||
@@ -468,7 +487,7 @@ impl ClientHandler {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(muxer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +704,7 @@ impl TunnelHandler for ServerHandler {
|
||||
let sid = log_session_id;
|
||||
let m = muxer.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(120)).await;
|
||||
tokio::time::sleep(SESSION_CLEANUP_DELAY).await;
|
||||
if m.active_legs_count() == 0 {
|
||||
sm.remove(&sid);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use tracing::instrument;
|
||||
use crate::{
|
||||
net::{
|
||||
connection::{handler::StreamHandler, muxer::MuxMessage},
|
||||
NetworkConfig, HEALTH_CHECK_INTERVAL,
|
||||
NetworkConfig, HEALTH_CHECK_INTERVAL, FALLBACK_CONNECT_TIMEOUT,
|
||||
RECONNECT_BACKOFF_BASE, RECONNECT_BACKOFF_JITTER_MS,
|
||||
TUNNEL_INTERLEAVE_CHUNK, TUNNEL_MAX_BUFFER_SIZE, TUNNEL_READ_RESERVE,
|
||||
},
|
||||
nrxp::{ErrorAction, FrameType, RxCodec, TxCodec, MAX_FRAME_PAYLOAD},
|
||||
};
|
||||
@@ -49,7 +51,7 @@ impl TunnelEngine {
|
||||
) -> Result<(OwnedReadHalf, OwnedWriteHalf, RxCodec, TxCodec), AppError> {
|
||||
info!("🔄 Attempting reconnect to {}", self.remote_addr);
|
||||
let stream = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(5),
|
||||
FALLBACK_CONNECT_TIMEOUT,
|
||||
tokio::net::TcpStream::connect(&self.remote_addr),
|
||||
)
|
||||
.await
|
||||
@@ -81,6 +83,14 @@ impl TunnelEngine {
|
||||
self.leg_status = LegStatus::Reconnecting;
|
||||
match self.attempt_reconnect().await {
|
||||
Ok((new_in, new_out, new_rx, new_tx)) => {
|
||||
let cap = crate::net::NetworkConfig::global().channel_capacity;
|
||||
let (control_tx, control_rx) =
|
||||
tokio::sync::mpsc::channel::<MuxMessage>(cap);
|
||||
let (data_tx, data_rx) = tokio::sync::mpsc::channel::<MuxMessage>(cap);
|
||||
self.muxer.add_leg(self.leg_id, control_tx, data_tx);
|
||||
self.control_rx = Some(control_rx);
|
||||
self.data_rx = Some(data_rx);
|
||||
|
||||
self.inbound = Some(new_in);
|
||||
self.outbound = Some(new_out);
|
||||
self.rx_codec = Some(new_rx);
|
||||
@@ -90,8 +100,8 @@ impl TunnelEngine {
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Reconnect failed for leg {}: {}", self.leg_id, e);
|
||||
let jitter = rand::random::<u64>() % 1000;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(2000 + jitter)).await;
|
||||
let jitter = rand::random::<u64>() % RECONNECT_BACKOFF_JITTER_MS;
|
||||
tokio::time::sleep(RECONNECT_BACKOFF_BASE + tokio::time::Duration::from_millis(jitter)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -119,10 +129,8 @@ impl TunnelEngine {
|
||||
let mut reader_handle = tokio::spawn(async move {
|
||||
let mut read_buf = read_buf;
|
||||
let mut inbound = inbound;
|
||||
const MAX_BUFFER_SIZE: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
loop {
|
||||
if read_buf.len() > MAX_BUFFER_SIZE {
|
||||
if read_buf.len() > TUNNEL_MAX_BUFFER_SIZE {
|
||||
error!("CRITICAL: Read buffer exceeded 1MB (OOM Protection). Dropping connection!");
|
||||
return Err(AppError::new(
|
||||
ERR_INFRA_TIMEOUT,
|
||||
@@ -134,7 +142,7 @@ impl TunnelEngine {
|
||||
if read_buf.is_empty() {
|
||||
read_buf.clear();
|
||||
}
|
||||
read_buf.reserve(16384);
|
||||
read_buf.reserve(TUNNEL_READ_RESERVE);
|
||||
|
||||
tokio::select! {
|
||||
_ = token_reader.cancelled() => {
|
||||
@@ -184,7 +192,7 @@ impl TunnelEngine {
|
||||
let mut heartbeat = tokio::time::interval(HEALTH_CHECK_INTERVAL);
|
||||
|
||||
let mut pending_data: Option<MuxMessage> = None;
|
||||
const INTERLEAVE_CHUNK: usize = 16384;
|
||||
let interleave_chunk = TUNNEL_INTERLEAVE_CHUNK;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -196,13 +204,24 @@ impl TunnelEngine {
|
||||
muxer_pong.record_ping_sent(leg_id);
|
||||
let msg = MuxMessage { stream_id: 0, frame_type: FrameType::Heartbeat, data: Bytes::new() };
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, msg).await {
|
||||
crate::net::diagnostics::send_diag_event(
|
||||
crate::net::diagnostics::DiagnosticsEvent::TunnelWriteStuck {
|
||||
leg_id, stream_id: 0,
|
||||
},
|
||||
);
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
}
|
||||
|
||||
msg_opt = control_rx.recv() => {
|
||||
if let Some(msg) = msg_opt {
|
||||
let sid = msg.stream_id;
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, msg).await {
|
||||
crate::net::diagnostics::send_diag_event(
|
||||
crate::net::diagnostics::DiagnosticsEvent::TunnelWriteStuck {
|
||||
leg_id, stream_id: sid,
|
||||
},
|
||||
);
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
} else { break; }
|
||||
@@ -212,7 +231,7 @@ impl TunnelEngine {
|
||||
_ = std::future::ready(()), if pending_data.is_some() => {
|
||||
let mut msg = pending_data.take().unwrap();
|
||||
|
||||
let chunk_size = std::cmp::min(msg.data.len(), INTERLEAVE_CHUNK);
|
||||
let chunk_size = std::cmp::min(msg.data.len(), interleave_chunk);
|
||||
let chunk_data = msg.data.split_to(chunk_size);
|
||||
|
||||
let chunk_msg = MuxMessage {
|
||||
@@ -220,8 +239,14 @@ impl TunnelEngine {
|
||||
frame_type: msg.frame_type.clone(),
|
||||
data: chunk_data,
|
||||
};
|
||||
let chunk_sid = chunk_msg.stream_id;
|
||||
|
||||
if let Err(e) = Self::handle_outbound(&mut outbound, &mut tx_codec, chunk_msg).await {
|
||||
crate::net::diagnostics::send_diag_event(
|
||||
crate::net::diagnostics::DiagnosticsEvent::TunnelWriteStuck {
|
||||
leg_id, stream_id: chunk_sid,
|
||||
},
|
||||
);
|
||||
return Err((e, control_rx, data_rx, tx_codec));
|
||||
}
|
||||
|
||||
@@ -366,6 +391,11 @@ impl TunnelEngine {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), write_future).await
|
||||
{
|
||||
error!(stream_id, "🔥 Physical leg STUCK on write. Killing leg.");
|
||||
// Increment counter; the call site in run() emits the full event
|
||||
// with the correct leg_id since handle_outbound is a static fn.
|
||||
crate::net::diagnostics::DIAG_COUNTERS
|
||||
.tunnel_write_stalls
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return Err(AppError::new(
|
||||
ERR_INFRA_TIMEOUT,
|
||||
"Таймаут отправки",
|
||||
|
||||
@@ -141,8 +141,8 @@ impl StreamHandler {
|
||||
}
|
||||
|
||||
FrameType::Data | FrameType::UdpData => {
|
||||
// Полностью синхронный вызов
|
||||
self.muxer.dispatch_to_local(stream_id, frame.payload);
|
||||
// MUST .await — maintains in-order delivery via back-pressure.
|
||||
self.muxer.dispatch_to_local(stream_id, frame.payload).await;
|
||||
}
|
||||
|
||||
FrameType::Close => {
|
||||
|
||||
@@ -5,4 +5,4 @@ mod handler;
|
||||
mod muxer;
|
||||
|
||||
pub use connection::{ClientHandler, Connection, ServerHandler, SessionManager, TunnelHandler};
|
||||
pub use muxer::GLOBAL_MIN_RTT;
|
||||
pub use muxer::{Muxer, GLOBAL_MIN_RTT};
|
||||
|
||||
@@ -7,7 +7,9 @@ use std::time::Instant;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::net::{INITIAL_RTT_MS, MUXER_CONGESTION_WEIGHT};
|
||||
use crate::net::{HEALTH_CHECK_TIMEOUT, MAX_TUNNEL_LEGS, MUXER_POOL_SIZE};
|
||||
use crate::net::diagnostics::{self, DiagnosticsEvent, DIAG_COUNTERS, LegMetrics, TunnelMetrics};
|
||||
use crate::nrxp::FrameType;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -62,7 +64,7 @@ pub struct MuxMessage {
|
||||
pub(crate) data: Bytes,
|
||||
}
|
||||
|
||||
pub static GLOBAL_MIN_RTT: AtomicU32 = AtomicU32::new(250);
|
||||
pub static GLOBAL_MIN_RTT: AtomicU32 = AtomicU32::new(INITIAL_RTT_MS);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Muxer {
|
||||
@@ -131,7 +133,7 @@ impl Muxer {
|
||||
.map_or(false, |leg| leg.control_tx.same_channel(tx));
|
||||
if should_remove {
|
||||
self.legs.remove(&leg_id);
|
||||
self.update_legs_cache(); // Обновляем Lock-Free кэш
|
||||
self.update_legs_cache();
|
||||
info!(
|
||||
leg_id,
|
||||
"MUXER: TCP leg removed safely, streams will re-balance"
|
||||
@@ -139,6 +141,13 @@ impl Muxer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn force_remove_leg(&self, leg_id: u32) {
|
||||
if self.legs.remove(&leg_id).is_some() {
|
||||
self.update_legs_cache();
|
||||
info!(leg_id, "MUXER: TCP leg force-removed on engine exit");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_all_legs(&self) {
|
||||
self.legs.clear();
|
||||
self.stream_bindings.clear();
|
||||
@@ -174,9 +183,9 @@ impl Muxer {
|
||||
.take(pool_size) // Берем только пул
|
||||
.min_by(|a, b| {
|
||||
let score_a = a.stats.rtt_ms.load(Ordering::Relaxed) as f64
|
||||
+ (a.congestion_factor() * 2000.0);
|
||||
+ (a.congestion_factor() * MUXER_CONGESTION_WEIGHT);
|
||||
let score_b = b.stats.rtt_ms.load(Ordering::Relaxed) as f64
|
||||
+ (b.congestion_factor() * 2000.0);
|
||||
+ (b.congestion_factor() * MUXER_CONGESTION_WEIGHT);
|
||||
score_a
|
||||
.partial_cmp(&score_b)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
@@ -244,36 +253,61 @@ impl Muxer {
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
DIAG_COUNTERS.upload_fails.fetch_add(1, Ordering::Relaxed);
|
||||
diagnostics::send_diag_event(DiagnosticsEvent::UploadFailed {
|
||||
stream_id,
|
||||
reason: "data channel closed (leg dropped)".into(),
|
||||
});
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 💡 КОНТРОЛЬ: Используем .try_send() для мгновенной приоритетной отправки (Non-blocking)
|
||||
match leg.control_tx.try_send(message) {
|
||||
Ok(_) => {
|
||||
leg.stats.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
stream_ref
|
||||
.value()
|
||||
.1
|
||||
.tx_bytes
|
||||
.fetch_add(size, Ordering::Relaxed);
|
||||
// Close and Heartbeat frames MUST be delivered reliably (.send().await).
|
||||
// Close: dropping it leaks stream resources.
|
||||
// Heartbeat (PONG): dropping it via try_send causes the health-check
|
||||
// probe to time out after HEALTH_CHECK_TIMEOUT and evict a live leg.
|
||||
let is_critical = matches!(message.frame_type, FrameType::Close | FrameType::Heartbeat);
|
||||
|
||||
if is_critical {
|
||||
match leg.control_tx.send(message).await {
|
||||
Ok(_) => {
|
||||
leg.stats.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
stream_ref.value().1.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => {
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
|
||||
// Канал контроля не должен забиваться. Если это произошло, пакет сбрасывается,
|
||||
// чтобы предотвратить зависание критических задач.
|
||||
netrunner_logger::warn!(
|
||||
stream_id,
|
||||
"Control queue FULL! Dropping control frame to avoid deadlock."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
} else {
|
||||
match leg.control_tx.try_send(message) {
|
||||
Ok(_) => {
|
||||
leg.stats.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(stream_ref) = self.streams.get(&stream_id) {
|
||||
stream_ref.value().1.tx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(ref dropped)) => {
|
||||
netrunner_logger::warn!(
|
||||
stream_id,
|
||||
"Control queue FULL! Dropping non-critical control frame."
|
||||
);
|
||||
DIAG_COUNTERS.control_full_drops.fetch_add(1, Ordering::Relaxed);
|
||||
diagnostics::send_diag_event(DiagnosticsEvent::ControlChannelFull {
|
||||
stream_id,
|
||||
frame_type: format!("{:?}", dropped.frame_type),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
self.remove_leg(leg.id, &leg.control_tx);
|
||||
Err(AppError::new(ERR_INFRA_TIMEOUT, "Обрыв", "Leg closed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,8 +362,11 @@ impl Muxer {
|
||||
self.stream_bindings.remove(&stream_id);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn dispatch_to_local(&self, stream_id: u32, data: Bytes) {
|
||||
// ORDERING CONTRACT: callers MUST .await this; the caller (TunnelEngine reader)
|
||||
// is a spawned task, so blocking here creates correct back-pressure all the way
|
||||
// back to the kernel TCP socket buffer. Never spawn a task to deliver data
|
||||
// from this function — that breaks in-order delivery guarantees.
|
||||
pub async fn dispatch_to_local(&self, stream_id: u32, data: Bytes) {
|
||||
let size = data.len() as u64;
|
||||
|
||||
let tx_and_stats = self.streams.get(&stream_id).map(|s| {
|
||||
@@ -338,24 +375,10 @@ impl Muxer {
|
||||
});
|
||||
|
||||
if let Some((tx, stats)) = tx_and_stats {
|
||||
match tx.try_send(data) {
|
||||
Ok(_) => {
|
||||
stats.rx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(data)) => {
|
||||
// Channel is full: spawn a task to wait for space.
|
||||
// This keeps the reader loop unblocked while providing
|
||||
// backpressure — the spawned future will be pending until
|
||||
// TcpConnection drains the channel and makes room.
|
||||
tokio::spawn(async move {
|
||||
if tx.send(data).await.is_ok() {
|
||||
stats.rx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
// Stream is gone; nothing to do.
|
||||
}
|
||||
// .send().await blocks until the receiver has space.
|
||||
// If the receiver is closed the error is silently ignored.
|
||||
if tx.send(data).await.is_ok() {
|
||||
stats.rx_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,17 +414,46 @@ impl Muxer {
|
||||
frame_type: FrameType::Heartbeat,
|
||||
data: Bytes::from("PING"),
|
||||
};
|
||||
if tx.try_send(msg).is_err() {
|
||||
self.remove_leg(leg_id, &tx);
|
||||
self.remove_stream(probe_stream_id);
|
||||
continue;
|
||||
match tx.try_send(msg) {
|
||||
Ok(_) => {}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
|
||||
// Writer is already dead — evict immediately without waiting 20s.
|
||||
warn!(leg_id, "Health check: control channel closed, evicting dead leg");
|
||||
self.remove_leg(leg_id, &tx);
|
||||
self.remove_stream(probe_stream_id);
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
|
||||
// Control channel is temporarily full: the writer is alive and
|
||||
// busy processing other frames. Skip this probe cycle — evicting
|
||||
// a healthy leg because its queue is momentarily saturated would
|
||||
// cause a spurious reconnect.
|
||||
self.remove_stream(probe_stream_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match tokio::time::timeout(crate::net::HEALTH_CHECK_TIMEOUT, probe_rx.recv()).await {
|
||||
Ok(Some(_)) => trace!(leg_id, "✅ TCP Leg Health Check OK"),
|
||||
_ => {
|
||||
warn!(leg_id, "❌ TCP Leg Health Check FAIL/Timeout - Evicting");
|
||||
self.remove_leg(leg_id, &tx);
|
||||
// Before evicting, verify the muxer still holds the same control_tx
|
||||
// we probed with. After an internal reconnect, add_leg replaces the
|
||||
// entry with new channels, and the old probe belongs to a dead leg
|
||||
// that the engine has already recycled — evicting the new leg here
|
||||
// would be wrong.
|
||||
let still_same = self
|
||||
.legs
|
||||
.get(&leg_id)
|
||||
.map_or(false, |l| l.control_tx.same_channel(&tx));
|
||||
if still_same {
|
||||
warn!(leg_id, "❌ TCP Leg Health Check FAIL/Timeout - Evicting");
|
||||
self.remove_leg(leg_id, &tx);
|
||||
} else {
|
||||
netrunner_logger::debug!(
|
||||
leg_id,
|
||||
"Health check probe timed out but leg already reconnected — skipping eviction"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.remove_stream(probe_stream_id);
|
||||
@@ -507,4 +559,40 @@ impl Muxer {
|
||||
}
|
||||
info!("\n{}", out);
|
||||
}
|
||||
|
||||
/// Collect a point-in-time snapshot of tunnel metrics for diagnostics.
|
||||
/// Lock-free: reads only atomics and the RwLock-protected legs cache.
|
||||
pub fn snapshot_tunnel_metrics(&self) -> TunnelMetrics {
|
||||
let global_min_rtt = crate::net::GLOBAL_MIN_RTT.load(Ordering::Relaxed);
|
||||
let cached_legs = self.active_legs_cache.read().unwrap().clone();
|
||||
|
||||
let active_legs: Vec<LegMetrics> = cached_legs
|
||||
.iter()
|
||||
.map(|leg| {
|
||||
let cap = leg.data_tx.max_capacity();
|
||||
let free = leg.data_tx.capacity();
|
||||
let filled = cap.saturating_sub(free);
|
||||
let congestion_factor = if cap > 0 {
|
||||
filled as f64 / cap as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
LegMetrics {
|
||||
leg_id: leg.id,
|
||||
rtt_ms: leg.stats.rtt_ms.load(Ordering::Relaxed),
|
||||
tx_mb: leg.stats.tx_bytes.load(Ordering::Relaxed) as f64 / 1_048_576.0,
|
||||
rx_mb: leg.stats.rx_bytes.load(Ordering::Relaxed) as f64 / 1_048_576.0,
|
||||
congestion_factor,
|
||||
data_channel_free: free,
|
||||
data_channel_capacity: cap,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
TunnelMetrics {
|
||||
global_min_rtt_ms: global_min_rtt,
|
||||
active_legs,
|
||||
total_streams: self.streams.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-14
@@ -1,32 +1,75 @@
|
||||
use std::time::Duration;
|
||||
|
||||
// ── Connection pool ──────────────────────────────────────────────────────────
|
||||
pub const MAX_SOCKETS: usize = 256;
|
||||
pub const MAX_TUNNEL_LEGS: u32 = 4;
|
||||
pub const MUXER_POOL_SIZE: usize = 3;
|
||||
/// Weight applied to observed congestion when scoring tunnel legs.
|
||||
pub const MUXER_CONGESTION_WEIGHT: f64 = 2000.0;
|
||||
/// Initial RTT estimate used before any real measurement arrives.
|
||||
pub const INITIAL_RTT_MS: u32 = 250;
|
||||
|
||||
// ── Timeouts ─────────────────────────────────────────────────────────────────
|
||||
pub const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
pub const UDP_IDLE_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
pub const GLOBAL_IDLE_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
|
||||
// 🔥 ФИКС: Ускоряем обнаружение мертвой сети при переключении Wi-Fi -> LTE
|
||||
pub const HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(3); // Было 7
|
||||
pub const HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(20); // Было 10
|
||||
pub const LEG_RECONNECT_DELAY: Duration = Duration::from_secs(2); // Было 3
|
||||
pub const HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(3);
|
||||
pub const HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
pub const LEG_RECONNECT_DELAY: Duration = Duration::from_secs(2);
|
||||
pub const BRIDGE_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const TLS_HELLO_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub const SECURE_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
pub const FALLBACK_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
/// Timeout for resolving a proxy address via DNS.
|
||||
pub const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
/// Delay between leg reconnect attempts (base); actual delay adds random jitter.
|
||||
pub const RECONNECT_BACKOFF_BASE: Duration = Duration::from_millis(2000);
|
||||
/// Upper bound of the random jitter added to `RECONNECT_BACKOFF_BASE`.
|
||||
pub const RECONNECT_BACKOFF_JITTER_MS: u64 = 1000;
|
||||
/// How long to wait before removing an idle session after all legs drop.
|
||||
pub const SESSION_CLEANUP_DELAY: Duration = Duration::from_secs(120);
|
||||
/// How often the network-change watcher checks the local IP address.
|
||||
pub const NETWORK_WATCHER_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub const DNS_PORT: u16 = 53;
|
||||
pub const HTTP_PORT: u16 = 80;
|
||||
pub const HTTPS_PORT: u16 = 443;
|
||||
pub const NETBIOS_PORTS: [u16; 2] = [137, 138];
|
||||
// ── Logging ───────────────────────────────────────────────────────────────────
|
||||
pub const LEG_STAGGER_DELAY: Duration = Duration::from_millis(1000);
|
||||
pub const TOPOLOGY_PRINT_INTERVAL: Duration = Duration::from_secs(10);
|
||||
/// How often the client engine logs traffic statistics.
|
||||
pub const STATS_LOG_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
// ── Authentication ───────────────────────────────────────────────────────────
|
||||
pub const AUTH_TIME_STEP: u64 = 60;
|
||||
pub const AUTH_WINDOW_SIZE: u64 = 2;
|
||||
|
||||
pub const LEG_STAGGER_DELAY: Duration = Duration::from_millis(1000); // Чуть ускорили старт
|
||||
pub const TOPOLOGY_PRINT_INTERVAL: Duration = Duration::from_secs(10);
|
||||
// ── Well-known ports ─────────────────────────────────────────────────────────
|
||||
pub const DNS_PORT: u16 = 53;
|
||||
pub const HTTP_PORT: u16 = 80;
|
||||
pub const HTTPS_PORT: u16 = 443;
|
||||
pub const HTTP_ALT_PORT: u16 = 8080;
|
||||
pub const SSH_PORT: u16 = 22;
|
||||
pub const RDP_PORT: u16 = 3389;
|
||||
pub const VNC_PORT: u16 = 5900;
|
||||
pub const RTMP_PORT: u16 = 1935;
|
||||
pub const NTP_PORT: u16 = 123;
|
||||
pub const NETBIOS_PORTS: [u16; 2] = [137, 138];
|
||||
|
||||
// ── TLS / stealth ────────────────────────────────────────────────────────────
|
||||
/// Hostname used as the SNI in the stealth TLS ClientHello.
|
||||
pub const STEALTH_FALLBACK_SNI: &str = "ubuntu.com";
|
||||
pub const STEALTH_FALLBACK_HOST: &str = "ubuntu.com:443";
|
||||
pub const FALLBACK_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub const TLS_HELLO_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub const SECURE_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
// ── Tunnel frame codec ───────────────────────────────────────────────────────
|
||||
/// OOM guard: drop the leg if the read buffer grows past this.
|
||||
pub const TUNNEL_MAX_BUFFER_SIZE: usize = 1024 * 1024;
|
||||
/// Bytes reserved in the read buffer before each `read_buf` call.
|
||||
pub const TUNNEL_READ_RESERVE: usize = 16 * 1024;
|
||||
/// Maximum bytes written per stream in a single interleaved write pass.
|
||||
pub const TUNNEL_INTERLEAVE_CHUNK: usize = 16 * 1024;
|
||||
|
||||
// ── Smoltcp socket defaults ──────────────────────────────────────────────────
|
||||
/// Packet slots for the ICMP socket's RX and TX packet buffers.
|
||||
pub const ICMP_META_SLOTS: usize = 4;
|
||||
/// Byte capacity of the ICMP socket's RX and TX data buffers.
|
||||
pub const ICMP_BUFFER_SIZE: usize = 512;
|
||||
/// Log a bufferbloat warning when the application-layer queue exceeds this.
|
||||
pub const BUFFERBLOAT_WARN_THRESHOLD: usize = 1024 * 1024;
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Mutex,
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// ── Public event channel ──────────────────────────────────────────────────────
|
||||
|
||||
/// Global sender end of the diagnostics event channel.
|
||||
/// Initialised once by `init_diagnostics()`; all producers call `send_diag_event()`.
|
||||
static GLOBAL_DIAG_TX: OnceLock<DiagnosisTx> = OnceLock::new();
|
||||
|
||||
pub type DiagnosisTx = mpsc::UnboundedSender<DiagnosticsEvent>;
|
||||
pub type DiagnosisRx = mpsc::UnboundedReceiver<DiagnosticsEvent>;
|
||||
|
||||
/// Call once at startup (from EngineBuilder on the client, from Network::run on
|
||||
/// the server). Returns the receiver end that the consumer task/engine must hold.
|
||||
pub fn init_diagnostics() -> DiagnosisRx {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
// Ignore the error if called twice (server may re-init across tests).
|
||||
let _ = GLOBAL_DIAG_TX.set(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
/// Fire-and-forget: enqueue a diagnostics event from anywhere in the codebase.
|
||||
/// Does nothing if `init_diagnostics()` has not been called yet.
|
||||
pub fn send_diag_event(event: DiagnosticsEvent) {
|
||||
if let Some(tx) = GLOBAL_DIAG_TX.get() {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event types ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// What triggered this diagnostics snapshot.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum DiagnosticsEvent {
|
||||
UploadFailed {
|
||||
stream_id: u32,
|
||||
reason: String,
|
||||
},
|
||||
DownloadBackpressure {
|
||||
stream_id: u32,
|
||||
/// How many background retry tasks are in flight for this stream.
|
||||
queued_tasks: usize,
|
||||
},
|
||||
LegDisconnected {
|
||||
leg_id: u32,
|
||||
rtt_ms: u32,
|
||||
reason: String,
|
||||
},
|
||||
LegReconnecting {
|
||||
leg_id: u32,
|
||||
attempt: u32,
|
||||
},
|
||||
StreamClosedWithError {
|
||||
stream_id: u32,
|
||||
up_bytes: u64,
|
||||
down_bytes: u64,
|
||||
error: String,
|
||||
},
|
||||
ControlChannelFull {
|
||||
stream_id: u32,
|
||||
frame_type: String,
|
||||
},
|
||||
TunnelWriteStuck {
|
||||
leg_id: u32,
|
||||
stream_id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Per-snapshot sub-structs (all Serialize) ──────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EngineMetrics {
|
||||
pub rx_total_mb: f64,
|
||||
pub tx_total_mb: f64,
|
||||
pub rx_speed_mb_s: f64,
|
||||
pub tx_speed_mb_s: f64,
|
||||
pub rx_packets: u64,
|
||||
pub tx_packets: u64,
|
||||
/// Current depth of the smoltcp ChannelDevice RX queue (packets).
|
||||
pub device_rx_queue_depth: usize,
|
||||
/// Current depth of the smoltcp ChannelDevice TX queue (packets).
|
||||
pub device_tx_queue_depth: usize,
|
||||
/// Free slots in the engine→TUN writer channel.
|
||||
pub tun_tx_channel_free: usize,
|
||||
/// Free slots in the TUN reader→engine channel.
|
||||
pub tun_rx_channel_free: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct LegMetrics {
|
||||
pub leg_id: u32,
|
||||
pub rtt_ms: u32,
|
||||
pub tx_mb: f64,
|
||||
pub rx_mb: f64,
|
||||
/// 0.0 = idle, 1.0 = data channel completely full.
|
||||
pub congestion_factor: f64,
|
||||
pub data_channel_free: usize,
|
||||
pub data_channel_capacity: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TunnelMetrics {
|
||||
pub global_min_rtt_ms: u32,
|
||||
pub active_legs: Vec<LegMetrics>,
|
||||
pub total_streams: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SocketMetrics {
|
||||
pub stream_id: u32,
|
||||
pub state: String,
|
||||
pub send_queue_bytes: usize,
|
||||
pub send_capacity_bytes: usize,
|
||||
pub recv_queue_bytes: usize,
|
||||
pub recv_capacity_bytes: usize,
|
||||
/// Bytes held in the single pending_chunk (partial write to smoltcp TX buf).
|
||||
pub pending_chunk_bytes: usize,
|
||||
/// Whether the upload path is currently blocked by smoltcp backpressure.
|
||||
pub tx_congested: bool,
|
||||
pub total_up_bytes: u64,
|
||||
pub total_down_bytes: u64,
|
||||
}
|
||||
|
||||
/// Cumulative error/event counters at the moment of the snapshot.
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct ErrorCounters {
|
||||
pub upload_fails: u64,
|
||||
pub download_backpressure_events: u64,
|
||||
pub leg_disconnects: u64,
|
||||
pub control_channel_full_drops: u64,
|
||||
pub tunnel_write_stalls: u64,
|
||||
pub stream_errors: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiagnosticsSnapshot {
|
||||
pub timestamp_ms: u64,
|
||||
pub trigger: DiagnosticsEvent,
|
||||
/// Engine-side metrics (traffic, device queue depths, channel free space).
|
||||
/// Present only on the client; `None` on server snapshots.
|
||||
pub engine: Option<EngineMetrics>,
|
||||
pub tunnel: TunnelMetrics,
|
||||
/// Per-socket smoltcp state. Present only on the client engine.
|
||||
pub sockets: Vec<SocketMetrics>,
|
||||
/// Running totals of all error/event counters up to this snapshot.
|
||||
pub error_totals: ErrorCounters,
|
||||
}
|
||||
|
||||
// ── Atomic error counters (global, updated at event sites) ───────────────────
|
||||
|
||||
pub struct DiagnosticsCounters {
|
||||
pub upload_fails: AtomicU64,
|
||||
pub download_backpressure: AtomicU64,
|
||||
pub leg_disconnects: AtomicU64,
|
||||
pub control_full_drops: AtomicU64,
|
||||
pub tunnel_write_stalls: AtomicU64,
|
||||
pub stream_errors: AtomicU64,
|
||||
}
|
||||
|
||||
impl DiagnosticsCounters {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
upload_fails: AtomicU64::new(0),
|
||||
download_backpressure: AtomicU64::new(0),
|
||||
leg_disconnects: AtomicU64::new(0),
|
||||
control_full_drops: AtomicU64::new(0),
|
||||
tunnel_write_stalls: AtomicU64::new(0),
|
||||
stream_errors: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> ErrorCounters {
|
||||
ErrorCounters {
|
||||
upload_fails: self.upload_fails.load(Ordering::Relaxed),
|
||||
download_backpressure_events: self.download_backpressure.load(Ordering::Relaxed),
|
||||
leg_disconnects: self.leg_disconnects.load(Ordering::Relaxed),
|
||||
control_channel_full_drops: self.control_full_drops.load(Ordering::Relaxed),
|
||||
tunnel_write_stalls: self.tunnel_write_stalls.load(Ordering::Relaxed),
|
||||
stream_errors: self.stream_errors.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process-global counters incremented at every event site.
|
||||
pub static DIAG_COUNTERS: DiagnosticsCounters = DiagnosticsCounters::new();
|
||||
|
||||
// ── DiagnosticsStore — holds the last N snapshots ─────────────────────────────
|
||||
|
||||
pub struct DiagnosticsStore {
|
||||
snapshots: Mutex<VecDeque<DiagnosticsSnapshot>>,
|
||||
max_snapshots: usize,
|
||||
}
|
||||
|
||||
impl DiagnosticsStore {
|
||||
pub fn new(max_snapshots: usize) -> Self {
|
||||
Self {
|
||||
snapshots: Mutex::new(VecDeque::new()),
|
||||
max_snapshots,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&self, snap: DiagnosticsSnapshot) {
|
||||
let mut q = self.snapshots.lock().unwrap();
|
||||
if q.len() >= self.max_snapshots {
|
||||
q.pop_front();
|
||||
}
|
||||
q.push_back(snap);
|
||||
}
|
||||
|
||||
/// Returns all stored snapshots as a pretty-printed JSON array.
|
||||
pub fn get_all_json(&self) -> String {
|
||||
let q = self.snapshots.lock().unwrap();
|
||||
let items: Vec<&DiagnosticsSnapshot> = q.iter().collect();
|
||||
serde_json::to_string_pretty(&items)
|
||||
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"))
|
||||
}
|
||||
|
||||
/// Returns only the most recent snapshot as pretty-printed JSON, or `"null"`.
|
||||
pub fn get_latest_json(&self) -> String {
|
||||
let q = self.snapshots.lock().unwrap();
|
||||
match q.back() {
|
||||
Some(s) => serde_json::to_string_pretty(s)
|
||||
.unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}")) ,
|
||||
None => "null".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn current_timestamp_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64
|
||||
}
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
mod config;
|
||||
mod connection;
|
||||
mod constants;
|
||||
pub mod diagnostics;
|
||||
|
||||
pub use config::NetworkConfig;
|
||||
pub use connection::{
|
||||
ClientHandler, Connection, ServerHandler, SessionManager, TunnelHandler, GLOBAL_MIN_RTT,
|
||||
ClientHandler, Connection, Muxer, ServerHandler, SessionManager, TunnelHandler, GLOBAL_MIN_RTT,
|
||||
};
|
||||
pub use constants::*;
|
||||
|
||||
Reference in New Issue
Block a user