diff --git a/README.md b/README.md index c2fbb51..ceaf95c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OSTP (Ospab Stealth Transport Protocol) +# OSTP — Ospab Stealth Transport Protocol [Русский язык](README.ru.md) @@ -6,30 +6,67 @@ ![License: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=flat-square) ![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=flat-square) -OSTP is a fast and secure transport protocol designed to bypass DPI and network restrictions. It masks traffic as high-entropy data, making it difficult to detect or block. +OSTP is a high-performance, censorship-resistant transport protocol designed to tunnel TCP traffic over UDP with full traffic obfuscation. It is resistant to Deep Packet Inspection (DPI), active probing, and statistical traffic analysis. --- -## Features +## Key Features -- **Traffic Obfuscation**: Hides VPN/proxy signatures from network analysis. -- **High Performance**: Written in Rust using the gVisor network stack for low latency. -- **Reliable Connectivity**: Built-in keep-alive mechanism for stable operation on mobile networks. -- **Flexible Modes**: Supports SOCKS5/HTTP proxying and full-system TUN (VPN) mode. -- **Multi-platform**: Compatible with Windows, Linux, macOS, and Android. +| Feature | Description | +|---------|-------------| +| **Traffic Obfuscation** | Every packet — including headers — is indistinguishable from random noise on the wire. Session IDs and nonces are masked with per-packet HMAC-derived keys. | +| **Noise Protocol Handshake** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — pre-shared key authenticated, forward-secret key exchange with no static identity exposure. | +| **Reliable UDP (ARQ)** | Selective ACK/NACK with rate-limited retransmission, configurable reorder buffer, and exponential backoff. Designed for 10 Gbps throughput. | +| **Multiplexed Streams** | Multiple logical TCP streams over a single encrypted UDP session, with per-stream flow control. | +| **Seamless Roaming** | Clients can switch networks (WiFi ↔ 4G) without session interruption — the server tracks session-ID, not IP address. | +| **TUN Mode** | Full-system VPN via `tun2socks` integration on Windows and Linux. All traffic is transparently routed through the tunnel. | +| **TURN Relay** | RFC 5766 TURN support for environments where direct UDP is blocked. | +| **Hot-Reload** | Runtime config reload without restarting the process (access keys, exclusions, mux settings, TURN). | +| **Cross-Platform** | Windows, Linux, macOS, Android. Single binary, no runtime dependencies. | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client │ +│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ +│ │ Browser │──▸│ SOCKS5/ │──▸│ Bridge (Mux) │ │ +│ │ / Apps │ │ HTTP │ │ ┌─────────────────┐ │ │ +│ │ │ │ Proxy │ │ │ ProtocolMachine │ │ │ +│ └──────────┘ └──────────┘ │ │ (Noise + AEAD) │ │ │ +│ │ └────────┬────────┘ │ │ +│ ┌──────────┐ │ │ │ │ +│ │ TUN Mode │──────────────────┤ UDP Socket │ │ +│ │tun2socks │ │ (32MB buffers, │ │ +│ └──────────┘ │ obfuscated wire) │ │ +│ └───────────┬────────────┘ │ +└────────────────────────────────────────────┼────────────────┘ + │ UDP +┌────────────────────────────────────────────┼────────────────┐ +│ Server │ │ +│ ┌─────────────────────────────────────────┴───────────┐ │ +│ │ Dispatcher │ │ +│ │ (Session lookup, roaming detection, replay guard) │ │ +│ └──────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────▾──────────────────┐ │ +│ │ Relay Loop (per-stream TCP) │──▸ Internet / Backend │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` --- ## Installation ### Linux -Run the installer script to set up OSTP as a system service: ```bash bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) ``` -### Windows -Run the following in PowerShell as Administrator: +### Windows (PowerShell, Administrator) ```powershell irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex ``` @@ -38,23 +75,23 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie ## Configuration -Initialize a default config file: +Generate a default config: ```bash -./ostp --init server # For VPS -./ostp --init client # For local machine +./ostp --init server # VPS +./ostp --init client # Local machine ``` -### Server (config.json) +### Server (`config.json`) ```jsonc { - // OSTP Server Configuration "mode": "server", "listen": "0.0.0.0:50000", - "access_keys": ["YOUR_KEY"], - // Optional: forward traffic to another proxy + "access_keys": ["YOUR_SECRET_KEY"], + "debug": false, + // Optional: forward traffic through an upstream proxy "outbound": { "enabled": false, - "protocol": "socks5", + "protocol": "socks5", // "socks5" or "http" "address": "127.0.0.1", "port": 9050, "default_action": "proxy" @@ -62,20 +99,35 @@ Initialize a default config file: } ``` -### Client (config.json) +### Client (`config.json`) ```jsonc { - // OSTP Client Configuration "mode": "client", - "server": "SERVER_IP:50000", - "access_key": "YOUR_KEY", + "server": "YOUR_SERVER_IP:50000", + "access_key": "YOUR_SECRET_KEY", "socks5_bind": "127.0.0.1:1088", - // Virtual network adapter settings + "debug": false, + // TUN mode (full-system VPN) "tun": { "enable": false, - "wintun_path": "./wintun.dll", - "ipv4_address": "10.1.0.2/24", "dns": "1.1.1.1" + }, + // Multiplexing: spread traffic across multiple UDP sessions + "mux": { + "enabled": false, + "sessions": 2 + }, + // TURN relay for restricted networks + "turn": { + "enabled": false, + "server_addr": "turn.example.com:3478", + "username": "user", + "access_key": "pass" + }, + // Traffic exclusions (bypassed directly) + "exclude": { + "domains": ["example.local"], + "ips": ["192.168.0.0/16"] } } ``` @@ -84,15 +136,65 @@ Initialize a default config file: ## Usage -Start the node with your configuration: ```bash +# Start with config ./ostp --config config.json + +# Or just run (looks for config.json in current/binary directory) +./ostp ``` -For TUN mode on Windows, ensure `tun2socks.exe` and `wintun.dll` are in the same directory. +### TUN Mode (Windows) +Requires `tun2socks.exe` in the same directory. Automatically requests Administrator privileges. + +### TUN Mode (Linux) +Requires root. Uses `tun2socks` binary (same directory or in `$PATH`). + +--- + +## Protocol Specification + +See [docs/en/specification.md](docs/en/specification.md) for the full wire format, handshake flow, and ARQ semantics. + +### Quick Summary + +| Layer | Mechanism | +|-------|-----------| +| Key Exchange | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) | +| Encryption | ChaCha20-Poly1305 AEAD per-packet | +| Header Obfuscation | HMAC-SHA256 derived per-packet mask over session_id + nonce | +| Reliability | Selective ACK with cumulative + SACK ranges | +| Retransmission | Rate-limited NACK (30ms cooldown) + exponential backoff RTO | +| Flow Control | In-flight window (retransmittable frames only) | +| Keepalive | Ping/Pong with RTT measurement every 5s | +| Session Timeout | 60s inactivity on client, 300s on server | + +--- + +## Building from Source + +```bash +# Prerequisites: Rust toolchain (1.75+) +cargo build --release + +# Cross-compile for Linux (from Windows/macOS) +cross build --release --target x86_64-unknown-linux-gnu +``` + +--- + +## Documentation + +- [Architecture Overview](docs/en/architecture.md) +- [Protocol Specification](docs/en/specification.md) +- [Obfuscation Design](docs/en/obfuscation.md) +- [Server Administration](docs/en/server.md) +- [Client Configuration](docs/en/client.md) +- [Integration Guide](docs/en/integrations.md) --- ## License -Business Source License 1.1. Free for personal and non-commercial use. Converts to MIT License on May 14, 2030. +Business Source License 1.1. Free for personal and non-commercial use. +Converts to MIT License on May 14, 2030. diff --git a/README.ru.md b/README.ru.md index b7bf379..0ce29ac 100644 --- a/README.ru.md +++ b/README.ru.md @@ -1,4 +1,4 @@ -# OSTP (Ospab Stealth Transport Protocol) +# OSTP — Ospab Stealth Transport Protocol [English](README.md) @@ -6,52 +6,89 @@ ![License: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=flat-square) ![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=flat-square) -OSTP — это быстрый и безопасный транспортный протокол для обхода DPI и сетевых ограничений. Он маскирует трафик под высокоэнтропийные данные, что делает его труднообнаружимым для систем блокировки. +OSTP — высокопроизводительный транспортный протокол, устойчивый к цензуре. Туннелирует TCP-трафик поверх UDP с полной обфускацией. Устойчив к Deep Packet Inspection (DPI), активному зондированию и статистическому анализу трафика. --- ## Возможности -- **Обфускация трафика**: Скрывает сигнатуры VPN и прокси от сетевого анализа. -- **Высокая производительность**: Написан на Rust с использованием сетевого стека gVisor. -- **Стабильность**: Встроенный механизм keep-alive для надежной работы в мобильных сетях. -- **Гибкость**: Поддержка проксирования SOCKS5/HTTP и полнофункционального TUN (VPN) режима. -- **Кроссплатформенность**: Работает на Windows, Linux, macOS и Android. +| Возможность | Описание | +|-------------|----------| +| **Обфускация трафика** | Каждый пакет, включая заголовки, неотличим от случайного шума. Session ID и nonce маскируются HMAC-ключами, уникальными для каждого пакета. | +| **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — аутентификация через PSK, forward secrecy, без раскрытия идентичности. | +| **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. Разработан для 10 Гбит/с. | +| **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. | +| **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. | +| **TUN-режим** | Полносистемный VPN через интеграцию с `tun2socks` на Windows и Linux. | +| **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. | +| **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). | +| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. | + +--- + +## Архитектура + +``` +┌────────────────────────────────────────────────────────────┐ +│ Клиент │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────────────┐ │ +│ │ Браузер │──▸│ SOCKS5/ │──▸│ Bridge (Mux) │ │ +│ │ / Прил. │ │ HTTP │ │ ┌─────────────────┐ │ │ +│ │ │ │ Прокси │ │ │ ProtocolMachine │ │ │ +│ └──────────┘ └──────────┘ │ │ (Noise + AEAD) │ │ │ +│ │ └────────┬────────┘ │ │ +│ ┌──────────┐ │ │ │ │ +│ │ TUN Mode │──────────────────┤ UDP-сокет │ │ +│ │tun2socks │ │ (32МБ буферы, │ │ +│ └──────────┘ │ обфускация) │ │ +│ └───────────┬────────────┘ │ +└────────────────────────────────────────────┼────────────────┘ + │ UDP +┌────────────────────────────────────────────┼────────────────┐ +│ Сервер │ │ +│ ┌─────────────────────────────────────────┴──────────┐ │ +│ │ Dispatcher │ │ +│ │ (Поиск сессий, роуминг, защита от replay) │ │ +│ └──────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────▾──────────────────┐ │ +│ │ Relay Loop (TCP per-stream) │──▸ Интернет / Backend │ +│ └─────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` --- ## Установка ### Linux -Используйте скрипт для автоматической установки и настройки сервиса: ```bash bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) ``` -### Windows -Запустите в PowerShell от имени администратора: +### Windows (PowerShell от Администратора) ```powershell irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex ``` --- -## Настройка +## Конфигурация -Создайте файл конфигурации по умолчанию: +Создать конфиг по умолчанию: ```bash -./ostp --init server # Для сервера (VPS) -./ostp --init client # Для клиента (ПК) +./ostp --init server # VPS +./ostp --init client # Локальная машина ``` -### Сервер (config.json) +### Сервер (`config.json`) ```jsonc { - // Конфигурация Сервера OSTP "mode": "server", "listen": "0.0.0.0:50000", "access_keys": ["ВАШ_КЛЮЧ"], - // Опционально: пересылка трафика через другой прокси + "debug": false, + // Опционально: проксировать трафик через upstream "outbound": { "enabled": false, "protocol": "socks5", @@ -62,20 +99,35 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie } ``` -### Клиент (config.json) +### Клиент (`config.json`) ```jsonc { - // Конфигурация Клиента OSTP "mode": "client", "server": "IP_СЕРВЕРА:50000", "access_key": "ВАШ_КЛЮЧ", "socks5_bind": "127.0.0.1:1088", - // Настройки виртуального сетевого адаптера + "debug": false, + // TUN-режим (полносистемный VPN) "tun": { "enable": false, - "wintun_path": "./wintun.dll", - "ipv4_address": "10.1.0.2/24", "dns": "1.1.1.1" + }, + // Мультиплексирование: несколько UDP-сессий + "mux": { + "enabled": false, + "sessions": 2 + }, + // TURN-реле для заблокированных сетей + "turn": { + "enabled": false, + "server_addr": "turn.example.com:3478", + "username": "user", + "access_key": "pass" + }, + // Исключения (идут напрямую, минуя туннель) + "exclude": { + "domains": ["example.local"], + "ips": ["192.168.0.0/16"] } } ``` @@ -84,15 +136,61 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie ## Использование -Запустите программу с вашим конфигом: ```bash +# Запуск с конфигом ./ostp --config config.json + +# Или просто (ищет config.json рядом с бинарником) +./ostp ``` -Для работы TUN режима в Windows файлы `tun2socks.exe` и `wintun.dll` должны находиться в одной папке с бинарным файлом. +### TUN-режим (Windows) +Требуется `tun2socks.exe` в той же директории. Автоматически запрашивает права Администратора. + +### TUN-режим (Linux) +Требуется root. Нужен бинарник `tun2socks` (рядом или в `$PATH`). + +--- + +## Спецификация протокола + +| Уровень | Механизм | +|---------|----------| +| Обмен ключами | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) | +| Шифрование | ChaCha20-Poly1305 AEAD на каждый пакет | +| Обфускация заголовков | HMAC-SHA256 маска session_id + nonce, уникальная для каждого пакета | +| Надёжность | Selective ACK с cumulative + SACK диапазонами | +| Ретрансмиссия | Rate-limited NACK (30мс cooldown) + exponential backoff RTO | +| Flow Control | Окно in-flight (только retransmittable фреймы) | +| Keepalive | Ping/Pong с измерением RTT каждые 5с | +| Таймаут сессии | 60с на клиенте, 300с на сервере | + +--- + +## Сборка из исходников + +```bash +# Требования: Rust toolchain (1.75+) +cargo build --release + +# Кросс-компиляция для Linux +cross build --release --target x86_64-unknown-linux-gnu +``` + +--- + +## Документация + +- [Архитектура](docs/ru/architecture.md) +- [Спецификация протокола](docs/ru/specification.md) +- [Дизайн обфускации](docs/ru/obfuscation.md) +- [Администрирование сервера](docs/ru/server.md) +- [Настройка клиента](docs/ru/client.md) +- [Интеграции](docs/ru/integrations.md) --- ## Лицензия -Business Source License 1.1. Бесплатно для личного и некоммерческого использования. Переходит в MIT License 14 мая 2030 года. +Business Source License 1.1. Бесплатно для личного и некоммерческого использования. +Переходит в MIT License 14 мая 2030 года. diff --git a/docs/en/specification.md b/docs/en/specification.md index 4915309..c24e0be 100644 --- a/docs/en/specification.md +++ b/docs/en/specification.md @@ -102,16 +102,23 @@ The initial handshake payload includes a Unix timestamp to mitigate replay attac ### 7.1 Selective-Repeat ARQ OSTP provides reliability over UDP using a **Selective-Repeat ARQ** mechanism: -* The receiver maintains a reorder buffer (default: 8192 packets). -* Unacknowledged packets are retransmitted after an adaptive Retransmission Time Out (RTO). -* Acknowledgments (ACKs) are piggybacked onto outbound data frames to minimize overhead. -* Backpressure is applied dynamically based on the number of in-flight unacknowledged frames. +* The receiver maintains a reorder buffer (default: 32768 packets) for out-of-order packet reassembly. +* Acknowledgments use a **Cumulative + SACK** scheme: the ACK payload contains a cumulative range `(0, expected_recv_nonce - 1)` confirming all contiguous packets received, plus up to 7 additional Selective ACK ranges for non-contiguous blocks in the reorder buffer. +* **Rate-limited NACK:** When a gap is detected, the receiver emits a NACK for the lowest missing nonce, but no more than once per 30ms. This prevents retransmission storms under normal UDP jitter. +* **Retransmission:** Unacknowledged data frames are retransmitted after an adaptive Retransmission Timeout (RTO, default: 100ms) with exponential backoff (up to 64× base RTO). +* **Zombie Frame Eviction:** Frames exceeding `max_retries + 4` attempts are automatically dropped from the send history, preventing unbounded memory consumption and stale retransmissions. +* **In-flight Counting:** Backpressure is based only on retransmittable (data) frames; control frames (ACK/NACK) are excluded from the in-flight count to prevent false backpressure under high load. +* **Graceful Close:** The `Closing` state processes all remaining in-flight packets before transitioning to `Closed`, preventing data loss during session teardown. ### 7.2 Adaptive Padding To resist traffic analysis via Packet Length Analysis (PLA), OSTP pads plaintext payloads before AEAD encryption. Padding bytes are drawn from a cryptographically secure random source. The protocol supports dynamic padding boundaries up to the maximum MTU (e.g., 1400 bytes), smoothing out recognizable application traffic bursts into constant-bitrate-like streams. ### 7.3 IP Roaming -The server supports seamless network handoffs (e.g., transitioning from Wi-Fi to cellular networks). If a packet successfully passes AEAD authentication, the server automatically binds the Session ID to the new source IP address without requiring a session restart. +The server supports seamless network handoffs (e.g., transitioning from Wi-Fi to cellular networks). If a packet successfully passes AEAD authentication, the server automatically binds the Session ID to the new source IP address without requiring a session restart. The server maintains a rate-limited roaming scanner (50 tokens/sec) to prevent CPU exhaustion from probing attacks. + +### 7.4 Session Keepalive +* **Client-side:** Ping/Pong frames with RTT measurement are sent every 5 seconds. If no valid UDP packet is received for 60 seconds, the client initiates reconnection. +* **Server-side:** Sessions with no activity for 300 seconds are automatically evicted. --- diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 3cb32c3..955633c 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -354,7 +354,7 @@ impl Bridge { _ = keepalive_tick.tick() => { if self.running { // 1. Connection Liveness Check - if self.last_valid_recv.elapsed().as_secs() > 30 { + if self.last_valid_recv.elapsed().as_secs() > 60 { let _ = tx.send(UiEvent::Log("Connection lost (timeout). Reconnecting...".into())).await; self.running = false; _proxy_guard = None; @@ -369,11 +369,13 @@ impl Bridge { // 2. Active Keep-Alive / Heartbeat if let Some(sessions) = sessions_opt.as_mut() { for session in sessions.iter_mut() { - // Send Ping (Internal Metric) + // Send Ping (Internal RTT Metric) let ts = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64; let ping_payload = Bytes::from(RelayMessage::Ping(ts).encode()); if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, ping_payload)) { - let _ = session.socket.send(&frame).await; + // Must go through send_datagram() for TURN-mode wrapping; + // raw socket.send() bypasses the ChannelData header and breaks RTT in TURN. + let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); } diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index 99fff7c..3e29139 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -129,6 +129,7 @@ struct RawUnifiedConfig { tun: Option, exclude: Option, mux: Option, + turn: Option, } #[derive(Debug, Deserialize)] @@ -150,6 +151,14 @@ struct RawMuxSection { sessions: Option, } +#[derive(Debug, Deserialize)] +struct RawTurnSection { + enabled: Option, + server_addr: Option, + username: Option, + access_key: Option, +} + impl ClientConfig { /// Hot-reload from `config.json` placed next to the running binary. /// Returns a new `ClientConfig` built from the unified JSON format. @@ -192,7 +201,15 @@ impl ClientConfig { bind_addr: socks5, connect_timeout_ms: 15000, }, - turn: TurnConfig::default(), + turn: match raw.turn { + Some(t) => TurnConfig { + enabled: t.enabled.unwrap_or(false), + server_addr: t.server_addr.unwrap_or_default(), + username: t.username.unwrap_or_default(), + access_key: t.access_key.unwrap_or_default(), + }, + None => TurnConfig::default(), + }, exclusions: ExclusionConfig { domains: exclusions.domains.unwrap_or_default(), ips: exclusions.ips.unwrap_or_default(), diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index 0c86038..855340a 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -279,15 +279,16 @@ fn format_bytes(bps: u64) -> String { fn is_essential_log(text: &str) -> bool { matches!( text, - "Handshaking started" - | "Bridge connection established" + "Connection established" | "TUN Tunnel established" | "Bridge stopped" | "TUN Tunnel stopped" | "Runtime config reloaded" + | "Connecting to remote server..." ) || text.starts_with("Connected UDP directly to ") || text.starts_with("TURN: Relay allocated") || text.starts_with("TURN allocation failed") - || text.starts_with("Handshake failed") - || text.starts_with("Connection timeout") + || text.starts_with("Connection failed:") + || text.starts_with("Connection lost") + || text.starts_with("Protocol tick fatal error") } diff --git a/ostp-client/src/tunnel/proxy.rs b/ostp-client/src/tunnel/proxy.rs index 284c16f..87818ed 100644 --- a/ostp-client/src/tunnel/proxy.rs +++ b/ostp-client/src/tunnel/proxy.rs @@ -43,8 +43,12 @@ pub async fn run_local_socks5_proxy( accepted = listener.accept() => { let (socket, _) = accepted?; let stream_id = next_stream_id; - next_stream_id = next_stream_id.wrapping_add(1); - if next_stream_id == 0 { next_stream_id = 1; } + // Advance, skipping zero and any stream_id still in active_streams + loop { + next_stream_id = next_stream_id.wrapping_add(1); + if next_stream_id == 0 { next_stream_id = 1; } + if !active_streams.contains_key(&next_stream_id) { break; } + } let (tx, rx) = mpsc::unbounded_channel(); active_streams.insert(stream_id, tx); @@ -229,12 +233,18 @@ async fn handle_proxy_client( // Read the rest of the HTTP request headers byte-by-byte let mut header_bytes = Vec::with_capacity(512); header_bytes.push(first_byte[0]); - let mut byte = [0_u8; 1]; + let mut chunk = [0_u8; 512]; loop { - client.read_exact(&mut byte).await?; - header_bytes.push(byte[0]); - if header_bytes.ends_with(b"\r\n\r\n") { - break; + let n = client.read(&mut chunk).await?; + if n == 0 { + return Err(anyhow!("connection closed during HTTP header read")); + } + header_bytes.extend_from_slice(&chunk[..n]); + if header_bytes.len() >= 4 { + let tail = &header_bytes[header_bytes.len().saturating_sub(4)..]; + if tail.ends_with(b"\r\n\r\n") { + break; + } } if header_bytes.len() > 8192 { client.write_all(b"HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n").await?; diff --git a/ostp-core/src/crypto/kex.rs b/ostp-core/src/crypto/kex.rs index 35567ee..32c2c11 100644 --- a/ostp-core/src/crypto/kex.rs +++ b/ostp-core/src/crypto/kex.rs @@ -1,7 +1,26 @@ -use rand::rngs::OsRng; -use sha2::{Digest, Sha256}; -use x25519_dalek::{EphemeralSecret, PublicKey}; +// ============================================================================= +// OSTP Hybrid Key Exchange — STUB / NOT IN USE +// ============================================================================= +// +// This module is a placeholder for future post-quantum key exchange. +// The actual key exchange is handled by the Noise NNpsk0 handshake in noise.rs. +// +// When ML-KEM (CRYSTALS-Kyber) support is added, this module will provide: +// 1. X25519 ephemeral DH (classical security) +// 2. ML-KEM-768 encapsulation (post-quantum security) +// 3. Combined shared secret = SHA-256(x25519_secret || ml_kem_secret) +// +// Until then, DO NOT use this module in production — it provides zero +// post-quantum security. The Noise handshake in noise.rs is the only +// active key exchange mechanism. +// ============================================================================= +#![allow(dead_code)] + +use sha2::{Digest, Sha256}; + +/// Placeholder shared secret output. +/// NOT USED by the protocol — provided for future API compatibility only. #[derive(Debug, Clone)] pub struct HybridSharedSecret { pub x25519_pubkey: [u8; 32], @@ -9,19 +28,27 @@ pub struct HybridSharedSecret { pub combined_secret: [u8; 32], } -pub trait KeyExchange { - fn client_kex() -> HybridSharedSecret; -} - +/// Placeholder hybrid key exchange. +/// The PQ component is a no-op stub. See module-level documentation. pub struct HybridKex; impl HybridKex { + /// Generate a hybrid key exchange offer. + /// + /// # Security Warning + /// The post-quantum component is a **stub** — `pq_ciphertext` is all zeros. + /// This function exists solely for API scaffolding. Do not rely on it for + /// post-quantum security. pub fn client_offer() -> HybridSharedSecret { + use rand::rngs::OsRng; + use x25519_dalek::{EphemeralSecret, PublicKey}; + let secret = EphemeralSecret::random_from_rng(OsRng); let pubkey = PublicKey::from(&secret); - // Placeholder PQ ciphertext. Replace with ML-KEM encapsulation output. + // TODO: Replace with ML-KEM-768 encapsulation (crate `ml-kem`) let pq_ciphertext = vec![0_u8; 1088]; + let mut hasher = Sha256::new(); hasher.update(pubkey.as_bytes()); hasher.update(&pq_ciphertext); @@ -37,9 +64,3 @@ impl HybridKex { } } } - -impl KeyExchange for HybridKex { - fn client_kex() -> HybridSharedSecret { - Self::client_offer() - } -} diff --git a/ostp-core/src/protocol.rs b/ostp-core/src/protocol.rs index f3b6d48..b2552b8 100644 --- a/ostp-core/src/protocol.rs +++ b/ostp-core/src/protocol.rs @@ -81,6 +81,8 @@ pub struct ProtocolMachine { max_sent_history: usize, ack_pending: bool, last_ack_sent: Instant, + /// Rate-limit: prevents sending a NACK more than once per 30ms to avoid storms + last_nack_sent: Instant, } #[derive(Debug, Clone)] @@ -121,11 +123,14 @@ impl ProtocolMachine { max_sent_history: config.max_sent_history.max(1), ack_pending: false, last_ack_sent: Instant::now(), + last_nack_sent: Instant::now() - Duration::from_secs(1), }) } pub fn in_flight_count(&self) -> usize { - self.sent_history.len() + // COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not + // contribute to this counter or they will trigger false backpressure. + self.sent_history.iter().filter(|f| f.is_retransmittable).count() } pub fn state(&self) -> OstpState { @@ -170,9 +175,12 @@ impl ProtocolMachine { self.build_tracked_datagram(0, FrameKind::Close, Bytes::new()) .map(ProtocolAction::SendDatagram) } - (OstpState::Closing, OstpEvent::Inbound(_)) => { + (OstpState::Closing, OstpEvent::Inbound(raw)) => { + // Process final in-flight packets to prevent data loss during teardown. + // The remote may still have data or ACKs in transit when we initiated Close. + let result = self.handle_inbound(raw); self.state = OstpState::Closed; - Ok(ProtocolAction::Noop) + result } (OstpState::Established, OstpEvent::Tick) => self.handle_tick(), (OstpState::Closed, _) => Ok(ProtocolAction::Noop), @@ -312,15 +320,21 @@ impl ProtocolMachine { })?; } } else { - // Gap detected! Buffer current packet and request immediate retransmit of the gap packet. + // Gap detected! Buffer current packet and request retransmit of the gap packet. if self.reorder_buffer.len() < self.max_reorder_buffer { self.reorder_buffer.insert(nonce, action); } - - // Emit a Nack frame for the lowest missing sequence - let nack_payload = self.expected_recv_nonce.to_be_bytes(); - if let Ok(nack_frame) = self.build_control_datagram(0, FrameKind::Nack, Bytes::copy_from_slice(&nack_payload)) { - outbound_actions.push(ProtocolAction::SendDatagram(nack_frame)); + + // 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); + if self.last_nack_sent.elapsed() >= nack_cooldown { + self.last_nack_sent = Instant::now(); + let nack_payload = self.expected_recv_nonce.to_be_bytes(); + if let Ok(nack_frame) = self.build_control_datagram(0, FrameKind::Nack, Bytes::copy_from_slice(&nack_payload)) { + outbound_actions.push(ProtocolAction::SendDatagram(nack_frame)); + } } } @@ -419,16 +433,22 @@ impl ProtocolMachine { let now = Instant::now(); let base_rto_ms = self.rto.as_millis().max(1) as u64; + + // Evict zombie frames that exceeded max_retries + grace period. + // Without eviction, unacknowledged frames accumulate forever, consuming memory + // and wasting bandwidth on retransmits that will never be acknowledged. + let grace = self.max_retries.saturating_add(4); + self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace); + for frame in self.sent_history.iter_mut() { if !frame.is_retransmittable { continue; } - if frame.retries == self.max_retries { + if frame.retries >= self.max_retries { tracing::warn!( - "Frame {} exceeded max retries ({}); continuing with backoff", - frame.nonce, - self.max_retries + "Frame nonce={} retry {}/{} (backoff active)", + frame.nonce, frame.retries, self.max_retries ); } @@ -517,7 +537,12 @@ impl ProtocolMachine { } if ranges.len() > MAX_RANGES { - ranges = ranges[ranges.len() - MAX_RANGES..].to_vec(); + // Always preserve the cumulative range (index 0) so the sender knows + // all frames up to expected_recv_nonce are received. Truncate SACK ranges. + let mut trimmed = vec![ranges[0]]; + let tail_start = ranges.len().saturating_sub(MAX_RANGES - 1); + trimmed.extend_from_slice(&ranges[tail_start..]); + ranges = trimmed; } let mut out = Vec::with_capacity(1 + ranges.len() * 16); diff --git a/ostp-server/src/dispatcher.rs b/ostp-server/src/dispatcher.rs index 46d416b..fbcac99 100644 --- a/ostp-server/src/dispatcher.rs +++ b/ostp-server/src/dispatcher.rs @@ -99,6 +99,10 @@ impl Dispatcher { if let Some(session_id) = session_id_opt { if let Some(peer_state) = self.peer_machines.get_mut(&session_id) { + // Update address on seamless roaming: remove old mapping to prevent HashMap leak + if peer_state.last_addr != peer { + self.addr_to_session.remove(&peer_state.last_addr); + } peer_state.last_addr = peer; peer_state.last_seen = std::time::Instant::now(); self.addr_to_session.insert(peer, session_id); @@ -204,6 +208,11 @@ impl Dispatcher { } if !self.replay_cache.contains_key(&payload.to_vec()) { + // Hard cap: prevent OOM under DDoS — replay cache grows + // unboundedly between purge ticks without this limit. + if self.replay_cache.len() >= 100_000 { + return Ok(DispatchOutcome::Unauthorized); + } // §4 fix: hard cap on concurrent sessions to prevent RAM exhaustion if self.peer_machines.len() >= MAX_SESSIONS { return Ok(DispatchOutcome::Unauthorized); diff --git a/ostp-server/src/lib.rs b/ostp-server/src/lib.rs index 436bd39..d8241b0 100644 --- a/ostp-server/src/lib.rs +++ b/ostp-server/src/lib.rs @@ -208,7 +208,9 @@ async fn run_server_loop( debug: bool, ) -> Result<()> { let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new(); - let (stream_tx, mut stream_rx) = mpsc::channel::<(u32, u16, Vec)>(10000); + // Unbounded channel: bounded(10000) caused TCP-reader tasks to fail under Speedtest load + // when 50+ streams competed for slots. Backpressure is managed at the relay layer instead. + let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec)>(); let (connect_tx, mut connect_rx) = mpsc::unbounded_channel::<(u32, u16, String, Result<(tokio::net::tcp::OwnedWriteHalf, mpsc::Sender<()>), String>)>(); let socket = std::sync::Arc::new(socket); @@ -388,7 +390,7 @@ async fn handle_relay_message( socket: &UdpSocket, remotes: &mut HashMap<(u32, u16), RemoteState>, ui_event_tx: &mpsc::UnboundedSender, - stream_tx: mpsc::Sender<(u32, u16, Vec)>, + stream_tx: mpsc::UnboundedSender<(u32, u16, Vec)>, connect_tx: mpsc::UnboundedSender<(u32, u16, String, Result<(tokio::net::tcp::OwnedWriteHalf, mpsc::Sender<()>), String>)>, outbound: Option, debug: bool, @@ -414,11 +416,11 @@ async fn handle_relay_message( read_res = reader.read(&mut buf) => { match read_res { Ok(0) | Err(_) => { - let _ = stream_tx_clone.send((session_id, stream_id, Vec::new())).await; + let _ = stream_tx_clone.send((session_id, stream_id, Vec::new())); break; } Ok(n) => { - if stream_tx_clone.send((session_id, stream_id, buf[..n].to_vec())).await.is_err() { + if stream_tx_clone.send((session_id, stream_id, buf[..n].to_vec())).is_err() { break; } } @@ -485,6 +487,7 @@ async fn connect_target( outbound: Option<&OutboundConfig>, debug: bool, ) -> Result { + let connect_timeout = Duration::from_secs(10); if let Some(outbound) = outbound { if outbound.enabled { let action = select_outbound_action(target, outbound, debug).await; @@ -493,13 +496,19 @@ async fn connect_target( return match outbound.protocol.as_str() { "socks5" => connect_via_socks5(&proxy_addr, target).await, "http" => connect_via_http(&proxy_addr, target).await, - _ => TcpStream::connect(target).await.map_err(Into::into), + _ => tokio::time::timeout(connect_timeout, TcpStream::connect(target)) + .await + .map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))? + .map_err(Into::into), }; } } } - TcpStream::connect(target).await.map_err(Into::into) + tokio::time::timeout(connect_timeout, TcpStream::connect(target)) + .await + .map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))? + .map_err(Into::into) } async fn select_outbound_action(