mirror of https://github.com/ospab/ostp.git
fix: resolve critical ARQ bugs causing Speedtest tunnel drops + docs overhaul
Critical fixes (6): - protocol.rs: in_flight_count() now counts only retransmittable Data frames, not Ack/Nack control frames — eliminates false backpressure under load - protocol.rs: NACK is now rate-limited to once per 30ms — prevents retransmission storm during normal UDP jitter - protocol.rs: zombie frames exceeding max_retries+4 are evicted each tick — prevents unbounded memory growth and stale retransmits - protocol.rs: Closing state now processes final in-flight packets instead of silently dropping them — prevents data loss at session teardown - server/lib.rs: stream_tx changed from bounded(10000) to unbounded_channel — prevents TCP-reader collapse during Speedtest with 50+ streams - bridge.rs: liveness timeout raised from 30s to 60s — prevents false reconnect during heavy Speedtest load Medium fixes (8): - protocol.rs: ACK range truncation preserves cumulative range (index 0) - bridge.rs: Ping now uses send_datagram() for correct TURN wrapping - dispatcher.rs: replay_cache hard-capped at 100k entries (DoS protection) - dispatcher.rs: old addr cleaned from addr_to_session on roaming - server/lib.rs: TCP connect_target() now has 10s timeout - config.rs: TURN section parsed during hot-reload - proxy.rs: HTTP header parsing uses 512-byte chunks instead of 1-byte reads - proxy.rs: stream_id wrap-around skips active IDs to prevent collision - runner.rs: is_essential_log matches actual log strings from bridge.rs Other: - kex.rs: clearly marked as dead PQ stub (not used by protocol) - README.md + README.ru.md: complete rewrite with architecture diagram - docs/en/specification.md: updated ARQ section with all new semantics
This commit is contained in:
parent
a9ba941782
commit
7424ccc0ff
160
README.md
160
README.md
|
|
@ -1,4 +1,4 @@
|
|||
# OSTP (Ospab Stealth Transport Protocol)
|
||||
# OSTP — Ospab Stealth Transport Protocol
|
||||
|
||||
[Русский язык](README.ru.md)
|
||||
|
||||
|
|
@ -6,30 +6,67 @@
|
|||

|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
148
README.ru.md
148
README.ru.md
|
|
@ -1,4 +1,4 @@
|
|||
# OSTP (Ospab Stealth Transport Protocol)
|
||||
# OSTP — Ospab Stealth Transport Protocol
|
||||
|
||||
[English](README.md)
|
||||
|
||||
|
|
@ -6,52 +6,89 @@
|
|||

|
||||

|
||||
|
||||
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 года.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ struct RawUnifiedConfig {
|
|||
tun: Option<RawTunSection>,
|
||||
exclude: Option<RawExcludeSection>,
|
||||
mux: Option<RawMuxSection>,
|
||||
turn: Option<RawTurnSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -150,6 +151,14 @@ struct RawMuxSection {
|
|||
sessions: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawTurnSection {
|
||||
enabled: Option<bool>,
|
||||
server_addr: Option<String>,
|
||||
username: Option<String>,
|
||||
access_key: Option<String>,
|
||||
}
|
||||
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<u8>)>(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<u8>)>();
|
||||
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<UiEvent>,
|
||||
stream_tx: mpsc::Sender<(u32, u16, Vec<u8>)>,
|
||||
stream_tx: mpsc::UnboundedSender<(u32, u16, Vec<u8>)>,
|
||||
connect_tx: mpsc::UnboundedSender<(u32, u16, String, Result<(tokio::net::tcp::OwnedWriteHalf, mpsc::Sender<()>), String>)>,
|
||||
outbound: Option<OutboundConfig>,
|
||||
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<TcpStream> {
|
||||
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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue