diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index a858471..e03bb6a 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -342,7 +342,7 @@ impl Bridge { Err(e) => { if is_uot { // TCP is dead — drop sender to signal bridge via channel close - tracing::warn!("UoT session {} disconnected: {}", session_index, e); + tracing::debug!("UoT session {} disconnected: {}", session_index, e); break; } else { tracing::warn!("UDP socket recv error (session {}): {}", session_index, e); @@ -436,7 +436,7 @@ impl Bridge { } Err(e) => { if is_uot { - tracing::warn!("UoT network-change session {} disconnected: {}", session_index, e); + tracing::debug!("UoT network-change session {} disconnected: {}", session_index, e); break; } else { tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e); @@ -574,7 +574,7 @@ impl Bridge { } Err(e) => { if is_uot { - tracing::warn!("UoT reconnect session {} disconnected: {}", session_index, e); + tracing::debug!("UoT reconnect session {} disconnected: {}", session_index, e); break; } else { tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e); diff --git a/ostp-client/src/sysproxy.rs b/ostp-client/src/sysproxy.rs index c08f38a..2573e4c 100644 --- a/ostp-client/src/sysproxy.rs +++ b/ostp-client/src/sysproxy.rs @@ -189,7 +189,7 @@ fn refresh_wininet() { #[cfg(not(target_os = "windows"))] pub fn enable_system_proxy(proxy_addr: &str) { let parts: Vec<&str> = proxy_addr.split(':').collect(); - let host = parts.get(0).unwrap_or(&"127.0.0.1"); + let host = parts.first().unwrap_or(&"127.0.0.1"); let port = parts.get(1).unwrap_or(&"1088"); let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok(); diff --git a/ostp-client/src/tunnel/udp_nat.rs b/ostp-client/src/tunnel/udp_nat.rs index 93ab9c2..62ca043 100644 --- a/ostp-client/src/tunnel/udp_nat.rs +++ b/ostp-client/src/tunnel/udp_nat.rs @@ -113,7 +113,7 @@ pub async fn run_udp_nat( async fn start_udp_bypass_session( client_src: SocketAddr, phys_if_index: Option, - phys_if_name: Option, + _phys_if_name: Option, session_rx: &mut mpsc::Receiver<(Vec, SocketAddr)>, smoltcp_tx: Arc>, ) -> anyhow::Result<()> { diff --git a/ostp-core/src/congestion.rs b/ostp-core/src/congestion.rs index 55fe7ee..22758d0 100644 --- a/ostp-core/src/congestion.rs +++ b/ostp-core/src/congestion.rs @@ -4,6 +4,12 @@ //! bandwidth and minimum RTT to determine the optimal sending rate. //! This replaces the fixed `retransmit_budget = 8` with an adaptive //! congestion window that responds to network conditions. +//! +//! RTO calculation follows RFC 6298: +//! SRTT = (1 - α) * SRTT + α * RTT (α = 1/8) +//! RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT| (β = 1/4) +//! RTO = SRTT + 4 * RTTVAR +//! clamped to [RTO_MIN, RTO_MAX] use std::time::{Duration, Instant}; @@ -15,8 +21,14 @@ pub struct CongestionController { ssthresh: u64, /// Current phase phase: Phase, - /// Minimum RTT observed + /// Minimum RTT observed (for BBR-style bandwidth estimation) min_rtt: Duration, + /// Smoothed RTT (RFC 6298 SRTT) + srtt: Duration, + /// RTT variance (RFC 6298 RTTVAR) + rttvar: Duration, + /// Whether we have received a first RTT sample + rtt_initialized: bool, /// Bytes currently in flight (unacknowledged) bytes_in_flight: u64, /// Total bytes acknowledged (for bandwidth estimation) @@ -37,31 +49,43 @@ pub struct CongestionController { enum Phase { /// Exponential growth until loss or ssthresh SlowStart, - /// Probe bandwidth: cycle through pacing gains + /// Probe bandwidth: additive increase ProbeBandwidth, } -/// Initial congestion window: 10 packets × MTU -const INITIAL_CWND_PACKETS: u64 = 10; +/// Initial congestion window: 32 packets × MTU (IW10 is too conservative for modern links) +const INITIAL_CWND_PACKETS: u64 = 32; /// Minimum cwnd: 2 packets const MIN_CWND_PACKETS: u64 = 2; /// Min RTT expiry window (after which we re-probe) const MIN_RTT_EXPIRY: Duration = Duration::from_secs(10); +/// Minimum RTO (RFC 6298: 1s in TCP; we use 50ms since we own the protocol) +const RTO_MIN: Duration = Duration::from_millis(50); +/// Maximum RTO +const RTO_MAX: Duration = Duration::from_secs(16); +/// Initial RTT estimate — 30 ms is reasonable for a well-connected VPN server. +/// Will be replaced by first real measurement within milliseconds. +const INITIAL_RTT: Duration = Duration::from_millis(30); impl CongestionController { pub fn new(mtu: u64) -> Self { let now = Instant::now(); let initial_cwnd = INITIAL_CWND_PACKETS * mtu; + // Initial pacing: deliver cwnd in ~2 RTTs to fill the pipe quickly + let initial_pacing = initial_cwnd * 1_000_000 / INITIAL_RTT.as_micros().max(1) as u64; Self { cwnd: initial_cwnd, ssthresh: u64::MAX, phase: Phase::SlowStart, - min_rtt: Duration::from_millis(100), // Conservative initial estimate + min_rtt: INITIAL_RTT, + srtt: INITIAL_RTT, + rttvar: INITIAL_RTT / 2, + rtt_initialized: false, bytes_in_flight: 0, total_acked: 0, last_ack_time: now, loss_count: 0, - pacing_rate: initial_cwnd * 10, // initial: ~10 windows/sec + pacing_rate: initial_pacing, mtu, min_rtt_stamp: now, } @@ -82,9 +106,20 @@ impl CongestionController { self.pacing_rate } - /// Returns the smoothed RTT estimate. + /// Returns the smoothed RTT estimate (SRTT). pub fn smoothed_rtt(&self) -> Duration { - self.min_rtt + self.srtt + } + + /// Returns the adaptive RTO computed per RFC 6298: + /// RTO = SRTT + 4 * RTTVAR, clamped to [RTO_MIN, RTO_MAX]. + /// + /// This replaces the static `rto_ms` field in ProtocolMachine so that + /// retransmit timers automatically track changing network conditions. + pub fn rto(&self) -> Duration { + let rttvar4 = self.rttvar.saturating_mul(4); + let rto = self.srtt.saturating_add(rttvar4); + rto.clamp(RTO_MIN, RTO_MAX) } /// Returns how many bytes can still be sent. @@ -115,16 +150,13 @@ impl CongestionController { self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes); self.total_acked = self.total_acked.saturating_add(bytes); - // Update RTT + // Update RTT measurements self.update_rtt(rtt, now); - // Update bandwidth estimate - self.update_bandwidth(bytes, now); - // State machine match self.phase { Phase::SlowStart => { - // Exponential growth: increase cwnd by acked bytes + // Exponential growth: increase cwnd by acked bytes (doubles per RTT) self.cwnd = self.cwnd.saturating_add(bytes); if self.cwnd >= self.ssthresh { self.phase = Phase::ProbeBandwidth; @@ -164,32 +196,49 @@ impl CongestionController { self.update_pacing_rate(); } - /// Called periodically to update state. - pub fn on_tick(&mut self) { - // Nothing special needed per-tick -- state updates happen on ACK/loss - } - // ── Private ────────────────────────────────────────────────────────────── fn update_rtt(&mut self, rtt: Duration, now: Instant) { - // Track windowed minimum RTT + // Update windowed minimum RTT (for pacing) if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY { self.min_rtt = rtt; self.min_rtt_stamp = now; } - } - fn update_bandwidth(&mut self, _acked_bytes: u64, now: Instant) { - let elapsed = now.duration_since(self.last_ack_time); - if elapsed.as_micros() > 0 { - // Removed bw_samples tracking + // Update SRTT and RTTVAR per RFC 6298 + if !self.rtt_initialized { + // First measurement: initialize directly + self.srtt = rtt; + self.rttvar = rtt / 2; + self.rtt_initialized = true; + } else { + // RTTVAR = (3/4) * RTTVAR + (1/4) * |SRTT - R| + let diff = if rtt > self.srtt { + rtt - self.srtt + } else { + self.srtt - rtt + }; + // Integer-safe: RTTVAR = RTTVAR - RTTVAR/4 + diff/4 + self.rttvar = self.rttvar + .saturating_sub(self.rttvar / 4) + .saturating_add(diff / 4); + + // SRTT = (7/8) * SRTT + (1/8) * R + self.srtt = self.srtt + .saturating_sub(self.srtt / 8) + .saturating_add(rtt / 8); } + + tracing::trace!( + srtt_ms = self.srtt.as_millis(), + rttvar_ms = self.rttvar.as_millis(), + rto_ms = self.rto().as_millis(), + "congestion: RTT updated" + ); } - - fn update_pacing_rate(&mut self) { - // Pacing rate = cwnd / min_rtt (with gain) + // Pacing rate = cwnd / min_rtt (delivery rate target) let rtt_us = self.min_rtt.as_micros().max(1) as u64; self.pacing_rate = self.cwnd * 1_000_000 / rtt_us; } @@ -202,19 +251,18 @@ mod tests { #[test] fn test_initial_state() { let cc = CongestionController::new(1200); - assert_eq!(cc.cwnd(), 12000); // 10 * 1200 + assert_eq!(cc.cwnd(), 32 * 1200); // 32 * 1200 assert!(cc.can_send()); - assert_eq!(cc.cwnd_packets(), 10); + assert_eq!(cc.cwnd_packets(), 32); } #[test] fn test_slow_start_growth() { let mut cc = CongestionController::new(1200); - // Simulate sending and ACKing + let initial = cc.cwnd(); cc.on_send(1200); cc.on_ack(1200, Duration::from_millis(50)); - // cwnd should grow - assert!(cc.cwnd() > 12000); + assert!(cc.cwnd() > initial); } #[test] @@ -229,7 +277,7 @@ mod tests { fn test_can_send_limits() { let mut cc = CongestionController::new(1200); // Send until cwnd is exhausted - for _ in 0..10 { + for _ in 0..32 { cc.on_send(1200); } assert!(!cc.can_send()); // cwnd exhausted @@ -244,10 +292,46 @@ mod tests { } #[test] - fn test_rtt_tracking() { + fn test_rtt_tracking_first_sample() { let mut cc = CongestionController::new(1200); cc.on_send(1200); cc.on_ack(1200, Duration::from_millis(25)); + // After first sample: SRTT = 25ms, RTTVAR = 12ms assert_eq!(cc.smoothed_rtt(), Duration::from_millis(25)); } + + #[test] + fn test_rto_rfc6298() { + let mut cc = CongestionController::new(1200); + // After first sample with RTT=50ms: SRTT=50ms, RTTVAR=25ms, RTO=150ms + cc.on_send(1200); + cc.on_ack(1200, Duration::from_millis(50)); + let rto = cc.rto(); + // RTO = 50 + 4*25 = 150ms; clamped to [50ms, 16s] + assert!(rto >= RTO_MIN); + assert!(rto <= RTO_MAX); + assert_eq!(rto, Duration::from_millis(150)); + } + + #[test] + fn test_rto_clamp_min() { + let cc = CongestionController::new(1200); + // Even with no RTT samples, RTO should not go below RTO_MIN + assert!(cc.rto() >= RTO_MIN); + } + + #[test] + fn test_rto_adapts_after_multiple_samples() { + let mut cc = CongestionController::new(1200); + // Feed several consistent RTT samples + for _ in 0..8 { + cc.on_send(1200); + cc.on_ack(1200, Duration::from_millis(20)); + } + // After convergence, RTTVAR should be small → RTO close to SRTT + small margin + let rto = cc.rto(); + // Should be well below 100ms (the old hardcoded default) + assert!(rto < Duration::from_millis(200)); + assert!(rto >= RTO_MIN); + } } diff --git a/ostp-core/src/protocol.rs b/ostp-core/src/protocol.rs index c0874d1..8ec23cf 100644 --- a/ostp-core/src/protocol.rs +++ b/ostp-core/src/protocol.rs @@ -395,18 +395,20 @@ impl ProtocolMachine { self.last_recv_advance = Instant::now(); } else { // Gap detected - if self.reorder_buffer.len() < self.max_reorder_buffer { - self.reorder_buffer.insert(nonce, action); + if nonce >= self.expected_recv_nonce { + if self.reorder_buffer.len() < self.max_reorder_buffer { + self.reorder_buffer.insert(nonce, action); + } else { + tracing::warn!("Reorder buffer still full after gap recovery, dropping frame nonce={}", nonce); + } } else { - tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}", - self.reorder_buffer.len(), self.max_reorder_buffer, nonce - ); + tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce); } - // Rate-limited NACK: send at most once per 30ms to prevent retransmit storms. - // Under high load with natural UDP reordering, sending a NACK per packet - // causes exponential retransmit explosion that saturates the channel. - let nack_cooldown = Duration::from_millis(30); + // Rate-limited NACK: send at most once per (rto/2) to prevent retransmit storms. + // Using rto/2 means we send a NACK before the sender's timer fires, prompting + // fast retransmit without flooding. Floor at 10ms to handle very low-RTT links. + let nack_cooldown = (self.cc.rto() / 2).max(Duration::from_millis(10)); if self.last_nack_sent.elapsed() >= nack_cooldown { self.last_nack_sent = Instant::now(); let nack_payload = self.expected_recv_nonce.to_be_bytes(); @@ -514,44 +516,18 @@ impl ProtocolMachine { fn handle_tick(&mut self) -> Result { let mut actions = Vec::new(); - // ── Gap Recovery ────────────────────────────────────────────── - // If expected_recv_nonce hasn't advanced for 500ms+ and there - // are buffered frames waiting, the sender likely evicted the lost - // frame from sent_history. Skip the gap to restore data flow. - // This trades a small amount of data loss for connection liveness. - if !self.reorder_buffer.is_empty() - && self.last_recv_advance.elapsed() > Duration::from_millis(500) - { - if let Some(&first_buffered) = self.reorder_buffer.keys().next() { - let skipped = first_buffered.saturating_sub(self.expected_recv_nonce); - self.expected_recv_nonce = first_buffered; - self.last_recv_advance = Instant::now(); - - let mut delivered = 0u64; - while let Some(buffered_action) = self.reorder_buffer.remove(&self.expected_recv_nonce) { - actions.push(buffered_action); - self.expected_recv_nonce = self.expected_recv_nonce.saturating_add(1); - delivered += 1; - } - self.ack_pending = true; - tracing::debug!("Gap recovery: skipped {} lost frames, delivered {} buffered frames (reorder_buf={})", - skipped, delivered, self.reorder_buffer.len() - ); - } - } - // ── Pending ACK flush ───────────────────────────────────────── if let Some(ack_frame) = self.build_ack_if_due()? { actions.push(ProtocolAction::SendDatagram(ack_frame)); } let now = Instant::now(); - let base_rto_ms = self.rto.as_millis().max(1) as u64; + // Use the adaptive RTO from the congestion controller (RFC 6298 SRTT + 4*RTTVAR). + // Falls back to rto_initial before the first ACK is received. + let base_rto_ms = self.cc.rto().max(self.rto).as_millis().max(1) as u64; // ── Zombie frame eviction ──────────────────────────────────── // Evict frames that exceeded max_retries + 2 grace retries. - // Shorter grace period than before (was +4) to free memory faster - // after high-throughput bursts. let grace = self.max_retries.saturating_add(2); let before = self.sent_history.len(); self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace); @@ -562,14 +538,15 @@ impl ProtocolMachine { // ── Retransmit expired frames ──────────────────────────────── // Limit retransmits per tick to prevent bandwidth saturation + // Backoff starts from retry #0 (immediately effective): + // effective_rto = base_rto * 2^retries, capped at 2^6 = 64× let mut retransmit_budget: usize = self.cc.retransmit_budget(); for frame in self.sent_history.iter_mut() { if !frame.is_retransmittable { continue; } - let retry_over = frame.retries.saturating_sub(self.max_retries); - let backoff_factor = 1u64 << retry_over.min(6); + let backoff_factor = 1u64 << (frame.retries as u64).min(6); let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor)); if now.duration_since(frame.last_sent) >= effective_rto { diff --git a/ostp-gui/src-tauri/Cargo.toml b/ostp-gui/src-tauri/Cargo.toml index 3e27815..9ad8d70 100644 --- a/ostp-gui/src-tauri/Cargo.toml +++ b/ostp-gui/src-tauri/Cargo.toml @@ -31,3 +31,4 @@ json_comments = "0.2" rand = "0.8" qrcode = { version = "0.14", default-features = false, features = ["svg"] } +rlimit = "0.11.0" diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 2a471a9..30bbe7e 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -789,8 +789,13 @@ pub fn run() { if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") { let _ = SINGLE_INSTANCE_LOCK.set(listener); } else { - show_error_dialog("Приложение OSTP GUI уже запущено!"); - return; + #[cfg(not(debug_assertions))] + { + show_error_dialog("Приложение OSTP GUI уже запущено!"); + return; + } + #[cfg(debug_assertions)] + println!("WARNING: OSTP GUI is already running, ignoring in debug mode."); } let state = AppState(Mutex::new(AppStateInner { tunnel: None })); diff --git a/ostp-gui/src-tauri/src/main.rs b/ostp-gui/src-tauri/src/main.rs index 83ad4ec..ea28969 100644 --- a/ostp-gui/src-tauri/src/main.rs +++ b/ostp-gui/src-tauri/src/main.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + let _ = rlimit::increase_nofile_limit(1048576); ostp_client::logging::setup_panic_hook(); // Read config BEFORE init_tracing so we can use the correct log level from config. diff --git a/ostp-gui/src/index.html b/ostp-gui/src/index.html index 42f80e6..88bdc22 100644 --- a/ostp-gui/src/index.html +++ b/ostp-gui/src/index.html @@ -6,52 +6,33 @@ OSTP - +
- - - - + - +
-
OSTP
- -
- +
-
Disconnected
-
Tap to protect your traffic
+
Disconnected
+
Tap to protect your traffic
- + + + +