From 90a919df599d2e0de7a6b2b595bbef46bcf4bf8a Mon Sep 17 00:00:00 2001 From: ospab Date: Sun, 12 Jul 2026 00:34:10 +0300 Subject: [PATCH] docs: update architecture diagram to be more understandable --- README.md | 59 ++++++++++++++++++++++----------------- README.ru.md | 57 +++++++++++++++++++++---------------- ostp-client/src/bridge.rs | 41 ++++++++++++++++++++++++--- ostp.wiki | 2 +- 4 files changed, 104 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 6c60745..078d91d 100644 --- a/README.md +++ b/README.md @@ -56,35 +56,42 @@ Download pre-built binaries for your platform from [GitHub Releases](https://git ## Architecture ```mermaid -graph TD - subgraph Client ["Client"] - A[Browser / Apps] -->|SOCKS5 / HTTP| B(Bridge Multiplexer) - TUN[TUN Interface] -->|IP Packets| B - - subgraph OSTPCoreClient ["OSTP Core Protocol"] - B --> C{Protocol Machine} - C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD] - D -->|Obfuscated UDP Payload| E((UDP Socket)) - end +flowchart LR + %% Styles + classDef userApp fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#01579b + classDef ostpCore fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#2e7d32 + classDef network fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100,stroke-dasharray: 5 5 + classDef external fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#4a148c + classDef fallback fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#c62828 + + subgraph Local["💻 Client Device"] + Apps["Web Browser / Apps"]:::userApp + Socks["SOCKS5 / HTTP Proxy"]:::ostpCore + Tun["Global TUN (VPN)"]:::ostpCore + Client["OSTP Client Protocol Engine\n(Noise + ChaCha20 + ARQ)"]:::ostpCore + + Apps -->|TCP/UDP| Socks + Apps -->|IP Packets| Tun + Socks --> Client + Tun --> Client end - E <==>|Encrypted & Obfuscated UDP Tunnel| F - - subgraph Server ["Server"] - F((UDP Socket)) --> G{Dispatcher} - - subgraph OSTPCoreServer ["OSTP Core Backend"] - G -->|Auth & Decrypt| H[Session & State Guard] - H -->|TCP Stream| I[Relay Loop] - end - - G -->|Active Probing / Unauth| FB[TCP Fallback Proxy] - FB -->|Forward| NGINX[nginx / Caddy] - - H -->|Stats & Traffic| API[Management API] - - I -->|Outbound| WWW((Internet)) + subgraph Internet["🌐 Hostile Network (DPI/Firewall)"] + Tunnel{"Fully Obfuscated\nEncrypted UDP\n(Looks like noise)"}:::network end + + subgraph Remote["🖥️ Remote VPS (Server)"] + Server["OSTP Server Protocol Engine\n(Authentication & Decryption)"]:::ostpCore + Relay["Connection Multiplexer"]:::ostpCore + Fallback["Fake Website\n(Nginx/Caddy)"]:::fallback + Target["Open Internet\n(YouTube, Google, etc)"]:::external + + Server -->|Decrypted Traffic| Relay + Server -->|Active Probe / Scanner| Fallback + Relay -->|Clear Traffic| Target + end + + Client <==> Tunnel <==> Server ``` --- diff --git a/README.ru.md b/README.ru.md index 7530435..16bb4cd 100644 --- a/README.ru.md +++ b/README.ru.md @@ -35,33 +35,42 @@ ## Архитектура ```mermaid -graph TD - subgraph Client ["Клиент"] - A[Браузер / Прил.] -->|SOCKS5 / HTTP| B(Bridge Multiplexer) - TUN[TUN Интерфейс] -->|IP Пакеты| B - - subgraph OSTPCoreClient ["OSTP Core Протокол"] - B --> C{Protocol Machine} - C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD] - D -->|Обфусцированный UDP| E((UDP Сокет)) - end +flowchart LR + %% Styles + classDef userApp fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#01579b + classDef ostpCore fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#2e7d32 + classDef network fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#e65100,stroke-dasharray: 5 5 + classDef external fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#4a148c + classDef fallback fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#c62828 + + subgraph Local["💻 Устройство клиента"] + Apps["Браузер / Приложения"]:::userApp + Socks["SOCKS5 / HTTP Прокси"]:::ostpCore + Tun["Global TUN (VPN)"]:::ostpCore + Client["OSTP Клиент\n(Noise + ChaCha20 + ARQ)"]:::ostpCore + + Apps -->|TCP/UDP| Socks + Apps -->|IP Пакеты| Tun + Socks --> Client + Tun --> Client end - E <==>|Зашифрованный UDP Туннель| F - - subgraph Server ["Сервер"] - F((UDP Сокет)) --> G{Dispatcher} - - subgraph OSTPCoreServer ["OSTP Core Backend"] - G -->|Auth & Decrypt| H[Session & State Guard] - H -->|TCP Поток| I[Relay Loop] - end - - G -->|Active Probing / Unauth| FB[TCP Fallback Proxy] - FB -->|Перенаправление| NGINX[nginx / Caddy] - - I -->|Outbound| WWW((Интернет)) + subgraph Internet["🌐 Сеть с цензурой (DPI)"] + Tunnel{"Зашифрованный UDP\n(Выглядит как белый шум)"}:::network end + + subgraph Remote["🖥️ Удаленный сервер (VPS)"] + Server["OSTP Сервер\n(Аутентификация)"]:::ostpCore + Relay["Мультиплексор соединений"]:::ostpCore + Fallback["Фейковый сайт\n(Nginx/Caddy)"]:::fallback + Target["Свободный интернет\n(YouTube, Google и т.д.)"]:::external + + Server -->|Расшифрованный трафик| Relay + Server -->|Сканеры цензоров| Fallback + Relay -->|Чистый трафик| Target + end + + Client <==> Tunnel <==> Server ``` --- diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 30259bf..14c383e 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -10,7 +10,7 @@ use ostp_core::{NoiseRole, OstpEvent, PaddingStrategy, ProtocolAction, ProtocolC use rand::Rng; use tokio::net::UdpSocket; use tokio::sync::{mpsc, watch}; -use tokio::time::{interval, timeout, Instant}; +use tokio::time::{interval, timeout, Instant, MissedTickBehavior}; use crate::app::{BridgeCommand, ConnectionStatus, UiEvent}; use crate::config::ClientConfig; @@ -131,6 +131,21 @@ impl Bridge { let mut metrics_tick = interval(Duration::from_millis(500)); let mut keepalive_tick = tokio::time::interval(Duration::from_secs(self.keepalive_interval_sec.max(1))); let mut retransmit_tick = tokio::time::interval(Duration::from_millis(10)); + // CRITICAL for suspend/resume: the default MissedTickBehavior is `Burst`, + // which after a laptop sleep or a phone backgrounding the app fires ALL + // the ticks that "should" have happened during the gap back-to-back. For + // the 10ms retransmit tick that is tens of thousands of instant ticks on + // resume — a CPU storm that hangs the bridge and manifests as the app + // freezing or getting stuck "Connecting". Skip missed ticks instead. + metrics_tick.set_missed_tick_behavior(MissedTickBehavior::Skip); + keepalive_tick.set_missed_tick_behavior(MissedTickBehavior::Skip); + retransmit_tick.set_missed_tick_behavior(MissedTickBehavior::Skip); + + // Wall-clock anchor for suspend/resume detection. tokio's timers run on a + // monotonic clock; comparing it against wall-clock lets us notice that + // the machine slept (or the app was frozen in the background) and force + // one clean reconnect instead of trying to resume a long-dead session. + let mut last_wall_check = SystemTime::now(); let init_msg = if self.mode == "tun" { "Bridge initialized (TUN mode)".to_string() } else { @@ -171,13 +186,27 @@ impl Bridge { } } _ = metrics_tick.tick() => { + // Suspend/resume detection: the wall clock jumps forward on + // wake even when the monotonic timer clock does not, so a + // large gap here means the machine slept / the app was frozen. + // The session is almost certainly dead (the server evicts + // idle sessions after 10 min), so force one clean reconnect + // rather than waiting on stale-session heuristics. + let wall_gap = last_wall_check.elapsed().unwrap_or_default(); + last_wall_check = SystemTime::now(); + if self.running && wall_gap > Duration::from_secs(15) { + let _ = tx.send(UiEvent::Log(format!( + "Resumed after ~{}s suspend — forcing clean reconnect", wall_gap.as_secs() + ))).await; + self.handle_keepalive(true, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx, &mut proxy_rx).await; + } if self.running { self.emit_metrics(&tx).await; } } _ = keepalive_tick.tick() => { if self.running { - self.handle_keepalive(&mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx, &mut proxy_rx).await; + self.handle_keepalive(false, &mut sessions_opt, &mut udp_rx_opt, &mut proxy_guard, &mut stream_map, &tx, &proxy_tx, &mut proxy_rx).await; } } _ = retransmit_tick.tick() => { @@ -523,6 +552,7 @@ impl Bridge { async fn handle_keepalive( &mut self, + force: bool, sessions_opt: &mut Option>, udp_rx_opt: &mut Option>, proxy_guard: &mut Option, @@ -531,9 +561,12 @@ impl Bridge { proxy_tx: &mpsc::UnboundedSender<(u16, ProxyToClientMsg)>, proxy_rx: &mut mpsc::Receiver, ) { - if self.last_valid_recv.elapsed().as_secs() > 25 { + if force || self.last_valid_recv.elapsed().as_secs() > 25 { let elapsed = self.last_valid_recv.elapsed().as_secs(); - if elapsed > 180 { + // On a forced (post-resume) reconnect the monotonic clock may not + // have advanced, so `elapsed` can be small — never treat a forced + // reconnect as a hard timeout; we specifically want to re-establish. + if !force && elapsed > 180 { if self.kill_switch { let _ = tx.send(UiEvent::Log(format!("Connection stall ({}s). Kill Switch is ON, retrying reconnect indefinitely...", elapsed))).await; } else { diff --git a/ostp.wiki b/ostp.wiki index 90810f2..2a22b52 160000 --- a/ostp.wiki +++ b/ostp.wiki @@ -1 +1 @@ -Subproject commit 90810f25f7af9e0a57bacce3d74ca3e46a6433e4 +Subproject commit 2a22b520b2112c35676537ad85b11804b0053f01