Compare commits

..

No commits in common. "master" and "v0.3.6" have entirely different histories.

131 changed files with 1640 additions and 4080 deletions

6
.gitignore vendored
View File

@ -36,9 +36,5 @@ turn-harvesting-idea.md
ostp-prober/ ostp-prober/
ostp-brain/ ostp-brain/
ostp-sandbox/ ostp-sandbox/
ostp-control/
ostp-license/ ostp-license/
ostp-web/
# Web panel
ostp-control/node_modules/
dnstt/

View File

@ -22,7 +22,6 @@ By contributing to this project, you agree to abide by our code of conduct and l
To build and test OSTP locally, you will need: To build and test OSTP locally, you will need:
* **Rust Toolchain**: Install via [rustup](https://rustup.rs/) (stable channel). * **Rust Toolchain**: Install via [rustup](https://rustup.rs/) (stable channel).
* **Go 1.20+**: Required to compile the embedded `dnstt` tunnel binaries.
* **Node.js (18+) & npm**: Required to compile Tauri GUI resources. * **Node.js (18+) & npm**: Required to compile Tauri GUI resources.
* **Git**: For version control. * **Git**: For version control.

View File

@ -22,7 +22,6 @@
Для локальной сборки и тестирования OSTP вам понадобятся: Для локальной сборки и тестирования OSTP вам понадобятся:
* **Rust Toolchain**: Установите через [rustup](https://rustup.rs/) (stable канал). * **Rust Toolchain**: Установите через [rustup](https://rustup.rs/) (stable канал).
* **Go 1.20+**: Необходимо для сборки встроенного DNS-туннеля dnstt.
* **Node.js (18+) и npm**: Необходимы для сборки интерфейса Tauri. * **Node.js (18+) и npm**: Необходимы для сборки интерфейса Tauri.
* **Git**: Для контроля версий. * **Git**: Для контроля версий.

74
Cargo.lock generated
View File

@ -388,16 +388,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
dependencies = [
"lazy-bytes-cast",
"winapi",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
@ -1262,12 +1252,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105"
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1447,18 +1431,16 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "ostp" name = "ostp"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"clap", "clap",
"clipboard-win",
"colored", "colored",
"json_comments", "json_comments",
"ostp-client", "ostp-client",
"ostp-core", "ostp-core",
"ostp-server", "ostp-server",
"pico-args",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
"serde", "serde",
@ -1471,7 +1453,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1506,20 +1488,17 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder",
"bytes", "bytes",
"chacha20poly1305", "chacha20poly1305",
"hkdf", "hkdf",
"hmac", "hmac",
"rand 0.8.5", "rand 0.8.5",
"serde",
"sha2", "sha2",
"snow", "snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio",
"tracing", "tracing",
"x25519-dalek", "x25519-dalek",
] ]
@ -1543,7 +1522,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-server" name = "ostp-server"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@ -1561,7 +1540,6 @@ dependencies = [
"portable-atomic", "portable-atomic",
"rand 0.8.5", "rand 0.8.5",
"reqwest", "reqwest",
"rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1576,7 +1554,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@ -1588,7 +1566,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun-helper" name = "ostp-tun-helper"
version = "0.3.12" version = "0.3.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1612,12 +1590,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1945,40 +1917,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2",
"walkdir",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"

View File

@ -12,7 +12,7 @@ resolver = "2"
[workspace.package] [workspace.package]
edition = "2021" edition = "2021"
license = "BSL 1.1" license = "BSL 1.1"
version = "0.3.12" version = "0.3.6"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0" anyhow = "1.0"

220
README.md
View File

@ -1,101 +1,131 @@
# OSTP — Ospab Stealth Transport Protocol # OSTP — Ospab Stealth Transport Protocol
[Русский язык](README.ru.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.md) · [Releases](https://github.com/ospab/ostp/releases) · [Migration Guide](docs/migration_v0_3_1.md) [Русский язык](README.ru.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.md) · [Releases](https://github.com/ospab/ostp/releases) · [Migration Guide](MIGRATION_V0_3_1.md)
![GitHub Release](https://img.shields.io/github/v/release/ospab/ostp?style=for-the-badge&color=blue) ![GitHub Release](https://img.shields.io/github/v/release/ospab/ostp?style=for-the-badge&color=blue)
![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=for-the-badge) ![License: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=for-the-badge)
![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=for-the-badge) ![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=for-the-badge)
![Crypto](https://img.shields.io/badge/Crypto-Noise__NNpsk0-blueviolet?style=for-the-badge) ![Crypto](https://img.shields.io/badge/Crypto-Noise__NNpsk0-blueviolet?style=for-the-badge)
![Transport](https://img.shields.io/badge/Transport-UDP%20ARQ-informational?style=for-the-badge) ![Transport](https://img.shields.io/badge/Transport-UDP%20ARQ-informational?style=for-the-badge)
OSTP (Ospab Stealth Transport Protocol) is an encrypted transport protocol written in Rust. It implements a custom ARQ transport over UDP and a UDP-over-TCP (UoT) mode. The protocol uses cryptographic masking for all packet headers and payloads to resist traffic classification by Deep Packet Inspection (DPI) systems. > A fast, custom encrypted transport protocol written in Rust.
**OSTP** (Ospab Stealth Transport Protocol) is a high-performance transport protocol. It implements a custom ARQ transport over UDP, as well as a UoT (UDP-over-TCP) mode. Every byte on the wire — including packet headers — is cryptographically indistinguishable from random noise, making it highly resistant to Deep Packet Inspection (DPI).
> [!IMPORTANT] > [!IMPORTANT]
> **Upgrading from v0.2.x?** Please read the [v0.3.1 Configuration Migration Guide](docs/migration_v0_3_1.md). > **Upgrading from v0.2.x?** Please read the [v0.3.1 Configuration Migration Guide](MIGRATION_V0_3_1.md).
--- ---
## Technical Capabilities ## Quick Install
| Capability | Description | ### Linux
|------------|-------------| ```bash
| **Traffic Masking** | Header and payload encryption using per-packet HMAC-derived keys. Indistinguishable from random noise. | bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)
| **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — PSK-authenticated, forward-secret key exchange. | ```
### Windows (PowerShell, run as Administrator)
```powershell
irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex
```
### Manual Download
Download pre-built binaries for your platform from [GitHub Releases](https://github.com/ospab/ostp/releases).
---
## Key Features
| Feature | Description |
|---------|-------------|
| **Full Traffic Obfuscation** | Every packet — including headers — is indistinguishable from random noise. Session IDs and nonces are masked with per-packet HMAC-derived keys. |
| **Noise Protocol Handshake** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — PSK-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. | | **Reliable UDP (ARQ)** | Selective ACK/NACK with rate-limited retransmission, configurable reorder buffer, and exponential backoff. |
| **Multiplexed Streams**| Multiple logical TCP streams over a single encrypted UDP session with per-stream flow control. | | **Multiplexed Streams** | Multiple logical TCP streams over a single encrypted UDP session with per-stream flow control. |
| **Session Roaming** | Connection persistence across IP changes via session ID tracking. | | **Seamless Roaming** | Clients can switch networks (WiFi ↔ LTE) without session interruption — tracked by session-ID, not IP. |
| **UoT Mode** | UDP-over-TCP encapsulation with length-prefixing to bypass UDP blocking. | | **Management API** | Built-in REST API for third-party panels (3x-ui, custom dashboards). Per-user stats, traffic limits, key CRUD. |
| **Fallback Server** | TCP proxying to a legitimate web server to resist active probing. | | **Fallback Server** | TCP fallback proxy to a web server — makes OSTP indistinguishable from nginx during active probing. |
| **TUN Mode** | Native network stack integration (`smoltcp`) for full-system routing without external dependencies. | | **Multi-Listener** | Bind to multiple addresses simultaneously (dual-stack IPv4/IPv6, multi-port). |
| **Management API** | Built-in REST API for server administration, metrics, and key generation. | | **TUN Mode** | Full-system VPN via native `smoltcp` network stack without external dependencies. All traffic transparently routed through the tunnel. |
| **TURN Relay** | RFC 5766 TURN support for NAT traversal. | | **xHTTP Stealth (UoT)** | UDP-over-TCP tunnel that completely hides traffic. Since all data is fully encrypted and length-prefixed, it bypasses DPI filters that block unknown UDP traffic by riding over a plain TCP connection. |
| **Mobile Apps** | Beautiful cross-platform mobile client (Flutter) for effortless client management. |
| **TURN Relay** | RFC 5766 TURN support for environments where direct UDP is blocked. |
| **Hot-Reload** | Runtime config reload without restart (access keys, exclusions, mux settings). |
| **Structured Logging** | `tracing`-based logging with `RUST_LOG` filtering. JSON/file/syslog output support. |
| **Cross-Platform** | Windows, Linux, macOS, Android, FreeBSD, MIPS, RISC-V. Single binary, no runtime dependencies. |
--- ---
## Architecture ## Architecture
```mermaid ```mermaid
flowchart LR graph TD
Apps[Local Apps] -->|SOCKS5 / TUN| CoreC subgraph Client ["Client"]
A[Browser / Apps] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
subgraph Client [Client Node] TUN[TUN Interface] -->|IP Packets| B
CoreC[OSTP Client] -.->|Encrypt & Mask| NetC[Transport Layer]
subgraph OSTPCoreClient ["OSTP Core Protocol"]
B --> C{Protocol Machine}
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Obfuscated UDP Payload| E((UDP Socket))
end
end end
NetC <==>|Encrypted UDP / UoT| NetS E <==>|Encrypted & Obfuscated UDP Tunnel| F
subgraph Server [Server Node] subgraph Server ["Server"]
NetS[Transport Layer] -.->|Decrypt & Auth| CoreS[OSTP Server] F((UDP Socket)) --> G{Dispatcher}
NetS -->|Unauthenticated| Fallback[Fallback Server]
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))
end end
CoreS -->|Relay| WWW((Internet))
Fallback -->|Forward| Web((Web / NGINX))
``` ```
--- ---
## Quick Start ## Quick Start
### 1. Installation ### 1. Generate config
**Linux:**
```bash ```bash
bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) # On your VPS (server):
```
**Windows (PowerShell as Administrator):**
```powershell
irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex
```
### 2. Configuration
Initialize the configuration files for the server and client:
```bash
# On the server:
./ostp --init server ./ostp --init server
# On the client: # On your machine (client):
./ostp --init client ./ostp --init client
``` ```
**Server Example** (`config.json`): ### 2. Edit config
**Server** — set your access keys:
```jsonc ```jsonc
{ {
"mode": "server", "mode": "server",
"listen": "0.0.0.0:50000", "listen": "0.0.0.0:50000",
"access_keys": ["YOUR_SECRET_KEY"] "access_keys": ["YOUR_SECRET_KEY"],
"api": { "enabled": true, "bind": "127.0.0.1:9090", "token": "admin-token" },
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" }
} }
``` ```
**Client Example** (`config.json`): **Client** — point to your server:
```jsonc ```jsonc
{ {
"mode": "client", "mode": "client",
"version": "0.3.1", "version": "0.3.1",
"log": { "level": "info" },
"inbounds": [ "inbounds": [
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 } { "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 },
{ "type": "tun", "tag": "tun-in", "auto_route": false, "mtu": 1140 }
], ],
"outbounds": [ "outbounds": [
{ {
@ -105,35 +135,90 @@ Initialize the configuration files for the server and client:
"port": 50000, "port": 50000,
"access_key": "YOUR_SECRET_KEY", "access_key": "YOUR_SECRET_KEY",
"transport": { "type": "udp" } "transport": { "type": "udp" }
} },
] { "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" }
],
"routing": {
"rules": [
{ "domain_suffix": ["localhost"], "outbound": "direct" }
],
"default_outbound": "proxy"
}
} }
``` ```
### 3. Execution > **Note:** Upgrading from v0.2.x? Read the [v0.3.1 Migration Guide](MIGRATION_V0_3_1.md).
### 3. Run
```bash ```bash
# Run with default config.json ./ostp # Uses config.json in current directory
./ostp ./ostp --config /path/to.json # Custom config path
./ostp --check # Validate config without running
# Run with a specific config path ./ostp --generate-key # Generate a new access key
./ostp --config /path/to/config.json ./ostp --links # Print client share links
``` ```
Or connect via a one-line share link on the client: ### 4. Connect via share link (one-liner)
```bash ```bash
./ostp "ostp://YOUR_SECRET_KEY@YOUR_SERVER_IP:50000?transport=udp" ./ostp "ostp://ACCESS_KEY@server.com:50000?..."
```
> [!WARNING]
> Always wrap the `ostp://...` link in quotes (`"`) so your terminal doesn't misinterpret special characters like `&` or `?`.
---
## Management API
Built-in REST API for building panels and dashboards.
```bash
# Server status
curl -H "Authorization: Bearer mytoken" http://127.0.0.1:9090/api/server/status
# List all users with traffic stats
curl -H "Authorization: Bearer mytoken" http://127.0.0.1:9090/api/users
# Create a user with 10GB traffic limit
curl -X POST -H "Authorization: Bearer mytoken" \
-H "Content-Type: application/json" \
-d '{"limit_bytes": 10737418240}' \
http://127.0.0.1:9090/api/users
```
Full API reference: [Management API](https://github.com/ospab/ostp/wiki/Management-API)
---
## CLI Reference
```
ostp [OPTIONS] [URL]
Options:
--config <PATH> Config file path (default: config.json)
--init <MODE> Generate template config (server/client)
--check Validate configuration and exit
-g, --generate-key Generate a secure access key
-c, --count <N> Number of keys to generate (default: 1)
--format <FMT> Key format: hex, base64 (default: hex)
--links Print client share links from server config
Arguments:
[URL] Connect via share link: ostp://KEY@HOST:PORT
``` ```
--- ---
## Protocol Specification ## Protocol Summary
| Layer | Mechanism | | Layer | Mechanism |
|-------|-----------| |-------|-----------|
| Key Exchange | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT | | Key Exchange | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT |
| Encryption | ChaCha20-Poly1305 AEAD per-packet | | Encryption | ChaCha20-Poly1305 AEAD per-packet |
| Header Masking | HMAC-SHA256 derived per-packet mask | | Header Obfuscation | HMAC-SHA256 derived per-packet mask |
| Reliability | Selective ACK with cumulative + SACK ranges | | Reliability | Selective ACK with cumulative + SACK ranges |
| Retransmission | Rate-limited NACK + exponential backoff RTO | | Retransmission | Rate-limited NACK + exponential backoff RTO |
| Keepalive | Ping/Pong with RTT measurement every 5s | | Keepalive | Ping/Pong with RTT measurement every 5s |
@ -143,31 +228,38 @@ Or connect via a one-line share link on the client:
## Building from Source ## Building from Source
```bash ```bash
# Requires Rust 1.75+ # Prerequisites: Rust 1.75+
cargo build --release cargo build --release
# Cross-compile for Linux # Cross-compile for Linux
cross build --release --target x86_64-unknown-linux-gnu cross build --release --target x86_64-unknown-linux-gnu
# Run tests
cargo test -p ostp-core -p ostp-server
``` ```
--- ---
## Documentation ## Documentation
- **[Wiki](https://github.com/ospab/ostp/wiki)** - **[Wiki](https://github.com/ospab/ostp/wiki)** — Full documentation
- [Installation](https://github.com/ospab/ostp/wiki/Installation)
- [Configuration Reference](https://github.com/ospab/ostp/wiki/Configuration) - [Configuration Reference](https://github.com/ospab/ostp/wiki/Configuration)
- [Management API](https://github.com/ospab/ostp/wiki/Management-API) - [Management API](https://github.com/ospab/ostp/wiki/Management-API)
- [Protocol Design](https://github.com/ospab/ostp/wiki/Protocol-Design) - [Protocol Design](https://github.com/ospab/ostp/wiki/Protocol-Design)
- [Building from Source](https://github.com/ospab/ostp/wiki/Building-from-Source)
- [FAQ](https://github.com/ospab/ostp/wiki/FAQ)
--- ---
## License ## License
GNU Affero General Public License v3.0 (AGPL-3.0). See [LICENSE](LICENSE) for more details. Business Source License 1.1. Free for personal and non-commercial use.
Converts to MIT License on May 14, 2030.
--- ---
## Contacts ## Contact
- **Telegram**: [@ospab0](https://t.me/ospab0) - **Telegram**: [@ospab0](https://t.me/ospab0)
- **Email**: gvoprgrg@gmail.com - **Email**: gvoprgrg@gmail.com

View File

@ -1,101 +1,123 @@
# OSTP — Ospab Stealth Transport Protocol # OSTP — Ospab Stealth Transport Protocol
[English](README.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.ru.md) · [Миграция v0.3.1](docs/migration_v0_3_1_ru.md) [English](README.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.ru.md) · [Миграция v0.3.1](MIGRATION_V0_3_1.md)
![GitHub Release](https://img.shields.io/github/v/release/ospab/ostp?style=for-the-badge&color=blue) ![GitHub Release](https://img.shields.io/github/v/release/ospab/ostp?style=for-the-badge&color=blue)
![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=for-the-badge) ![License: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=for-the-badge)
![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=for-the-badge) ![Platform: Windows | Linux | macOS | Android](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-green.svg?style=for-the-badge)
![Crypto](https://img.shields.io/badge/Crypto-Noise__NNpsk0-blueviolet?style=for-the-badge) ![Crypto](https://img.shields.io/badge/Crypto-Noise__NNpsk0-blueviolet?style=for-the-badge)
![Transport](https://img.shields.io/badge/Transport-UDP%20ARQ-informational?style=for-the-badge) ![Transport](https://img.shields.io/badge/Transport-UDP%20ARQ-informational?style=for-the-badge)
OSTP (Ospab Stealth Transport Protocol) — зашифрованный транспортный протокол, написанный на Rust. Реализует механизм ARQ поверх UDP, а также режим UoT (UDP-over-TCP). Протокол использует криптографическое маскирование заголовков и полезной нагрузки для защиты от систем глубокого анализа трафика (DPI). > Быстрый кастомный зашифрованный транспортный протокол на Rust.
**OSTP** (Ospab Stealth Transport Protocol) — кастомный транспортный протокол. Реализует собственный ARQ-транспорт поверх UDP, а также режим UoT (UDP-over-TCP). Каждый байт, включая заголовки пакетов, криптографически неотличим от случайного шума, что делает его устойчивым к системам глубокого анализа трафика (DPI).
> [!IMPORTANT] > [!IMPORTANT]
> **Обновляетесь с версии v0.2.x?** Пожалуйста, ознакомьтесь с [Руководством по миграции конфигурации v0.3.1](docs/migration_v0_3_1_ru.md). > **Обновляетесь с версии v0.2.x?** Пожалуйста, ознакомьтесь с [Руководством по миграции конфигурации v0.3.1](MIGRATION_V0_3_1.md).
--- ---
## Технические характеристики ## Возможности
| Возможность | Описание | | Возможность | Описание |
|-------------|----------| |-------------|----------|
| **Маскирование трафика** | Шифрование заголовков и полезной нагрузки с помощью HMAC ключей на каждый пакет. Трафик неотличим от шума. | | **Обфускация трафика** | Каждый пакет, включая заголовки, неотличим от случайного шума. Session ID и nonce маскируются HMAC-ключами, уникальными для каждого пакета. |
| **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — аутентификация через PSK, forward secrecy. | | **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — аутентификация через PSK, forward secrecy, без раскрытия идентичности. |
| **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. | | **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. Разработан для 10 Гбит/с. |
| **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. | | **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. |
| **Session Roaming** | Сохранение соединения при смене IP-адреса благодаря отслеживанию по идентификатору сессии (session ID). | | **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. |
| **Режим UoT** | Инкапсуляция UDP внутри TCP с указанием длины пакетов для обхода блокировок неизвестного UDP-трафика. | | **TUN-режим** | Полносистемный VPN без внешних зависимостей (встроенный network stack на базе `smoltcp`). |
| **Fallback Server** | Проксирование неаутентифицированных TCP подключений на веб-сервер для защиты от активного пробинга. | | **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, который полностью скрывает трафик. Поскольку все данные полностью зашифрованы и имеют префикс длины, он обходит DPI фильтры, блокирующие неизвестный UDP трафик, передавая всё по обычному TCP соединению. |
| **TUN-режим** | Полносистемная маршрутизация через встроенный сетевой стек `smoltcp` без внешних зависимостей. | | **Мобильные приложения** | Красивый кроссплатформенный мобильный клиент (Flutter) для удобного администрирования. |
| **Management API** | Встроенный REST API для администрирования сервера, сбора метрик и генерации ключей. | | **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. |
| **TURN Relay** | Поддержка RFC 5766 TURN для обхода NAT. | | **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). |
| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. |
--- ---
## Архитектура ## Архитектура
```mermaid ```mermaid
flowchart LR graph TD
Apps[Приложения] -->|SOCKS5 / TUN| CoreC subgraph Client ["Клиент"]
A[Браузер / Прил.] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
subgraph Client [Клиент] TUN[TUN Интерфейс] -->|IP Пакеты| B
CoreC[OSTP Клиент] -.->|Шифрование| NetC[Транспортный уровень]
subgraph OSTPCoreClient ["OSTP Core Протокол"]
B --> C{Protocol Machine}
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Обфусцированный UDP| E((UDP Сокет))
end
end end
NetC <==>|Зашифрованный UDP / UoT| NetS E <==>|Зашифрованный UDP Туннель| F
subgraph Server [Сервер] subgraph Server ["Сервер"]
NetS[Транспортный уровень] -.->|Дешифрование| CoreS[OSTP Сервер] F((UDP Сокет)) --> G{Dispatcher}
NetS -->|Неавторизованные| Fallback[Fallback Сервер]
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((Интернет))
end end
CoreS -->|Проксирование| WWW((Интернет))
Fallback -->|Перенаправление| Web((Веб-сервер / NGINX))
``` ```
--- ---
## Быстрый старт ## Установка
### 1. Установка ### Linux
**Linux:**
```bash ```bash
bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)
``` ```
**Windows (PowerShell от Администратора):** ### Windows (PowerShell от Администратора)
```powershell ```powershell
irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex
``` ```
### 2. Конфигурация ---
Сгенерируйте базовые файлы конфигурации: ## Конфигурация
Создать конфиг по умолчанию:
```bash ```bash
# На сервере: ./ostp --init server # VPS
./ostp --init server ./ostp --init client # Локальная машина
# На клиенте:
./ostp --init client
``` ```
**Пример конфигурации сервера** (`config.json`): ### Сервер (`config.json`)
```jsonc ```jsonc
{ {
"mode": "server", "mode": "server",
"listen": "0.0.0.0:50000", "listen": "0.0.0.0:50000",
"access_keys": ["ВАШ_КЛЮЧ"] "access_keys": ["ВАШ_КЛЮЧ"],
"debug": false,
// Опционально: проксировать трафик через upstream
"outbound": {
"enabled": false,
"protocol": "socks5",
"address": "127.0.0.1",
"port": 9050,
"default_action": "proxy"
}
} }
``` ```
**Пример конфигурации клиента** (`config.json`): ### Клиент (`config.json`)
```jsonc ```jsonc
{ {
"mode": "client", "mode": "client",
"version": "0.3.1", "version": "0.3.1",
"log": { "level": "info" },
"inbounds": [ "inbounds": [
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 } { "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 },
{ "type": "tun", "tag": "tun-in", "auto_route": false, "mtu": 1140 }
], ],
"outbounds": [ "outbounds": [
{ {
@ -104,26 +126,41 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
"server": "IP_СЕРВЕРА", "server": "IP_СЕРВЕРА",
"port": 50000, "port": 50000,
"access_key": "ВАШ_КЛЮЧ", "access_key": "ВАШ_КЛЮЧ",
"transport": { "type": "udp" } "transport": { "type": "udp" },
} "multiplex": { "enabled": false, "sessions": 1 }
] },
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" }
],
"routing": {
"rules": [
{ "domain_suffix": ["example.local"], "outbound": "direct" },
{ "ip_cidr": ["192.168.0.0/16"], "outbound": "direct" }
],
"default_outbound": "proxy"
}
} }
``` ```
### 3. Запуск > **Примечание:** Обновляетесь с v0.2.x? Прочтите [Гайд по миграции на v0.3.1](MIGRATION_V0_3_1.md).
---
## Использование
```bash ```bash
# Запуск с конфигурацией по умолчанию (config.json) # Запуск с конфигом
./ostp --config config.json
# Или просто (ищет config.json рядом с бинарником)
./ostp ./ostp
# Запуск с указанием пути к конфигурации
./ostp --config /path/to/config.json
``` ```
Либо подключение через однострочную ссылку на стороне клиента: ### TUN-режим (Windows)
```bash Использует встроенный сетевой стек `smoltcp` и виртуальный адаптер `wintun` (необходима `wintun.dll`). Требует запуска с правами Администратора.
./ostp "ostp://ВАШ_КЛЮЧ@IP_СЕРВЕРА:50000?transport=udp"
``` ### TUN-режим (Linux)
Использует встроенный сетевой стек `smoltcp` и `/dev/net/tun`. Требует запуска от имени `root` (или наличия `CAP_NET_ADMIN`).
--- ---
@ -133,22 +170,19 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
|---------|----------| |---------|----------|
| Обмен ключами | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT | | Обмен ключами | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT |
| Шифрование | ChaCha20-Poly1305 AEAD на каждый пакет | | Шифрование | ChaCha20-Poly1305 AEAD на каждый пакет |
| Обфускация заголовков | HMAC-SHA256 маска на основе session_id и nonce | | Обфускация заголовков | HMAC-SHA256 маска session_id + nonce, уникальная для каждого пакета |
| Надёжность | Selective ACK с cumulative + SACK диапазонами | | Надёжность | Selective ACK с cumulative + SACK диапазонами |
| Ретрансмиссия | Rate-limited NACK + exponential backoff RTO | | Ретрансмиссия | Rate-limited NACK (30мс cooldown) + exponential backoff RTO |
| Flow Control | Окно in-flight (только retransmittable фреймы) |
| Keepalive | Ping/Pong с измерением RTT каждые 5с | | Keepalive | Ping/Pong с измерением RTT каждые 5с |
| Таймаут сессии | 60с на клиенте, 300с на сервере |
--- ---
## Сборка из исходников ## Сборка из исходников
### Зависимости для сборки
- Rust 1.70+
- Go 1.20+ (необходимо для сборки встроенного DNS-туннеля dnstt)
> **Благодарности:** Этот проект использует [dnstt](https://www.bamsoftware.com/software/dnstt/) от Bamsoftware для обеспечения устойчивого туннелирования поверх DNS. Бинарники dnstt автоматически компилируются и встраиваются в ядро OSTP.
```bash ```bash
# Требования: Rust toolchain (1.75+)
cargo build --release cargo build --release
# Кросс-компиляция для Linux # Кросс-компиляция для Linux
@ -159,21 +193,16 @@ cross build --release --target x86_64-unknown-linux-gnu
## Документация ## Документация
- **[Wiki](https://github.com/ospab/ostp/wiki)** - [Архитектура](docs/ru/architecture.md)
- [Спецификация протокола](docs/ru/specification.md) - [Спецификация протокола](docs/ru/specification.md)
- [Дизайн обфускации](docs/ru/obfuscation.md) - [Дизайн обфускации](docs/ru/obfuscation.md)
- [Архитектура](docs/ru/architecture.md) - [Администрирование сервера](docs/ru/server.md)
- [Настройка клиента](docs/ru/client.md) - [Настройка клиента](docs/ru/client.md)
- [Интеграции](docs/ru/integrations.md)
--- ---
## Лицензия ## Лицензия
GNU Affero General Public License v3.0 (AGPL-3.0). Подробнее см. в файле [LICENSE](LICENSE). Business Source License 1.1. Бесплатно для личного и некоммерческого использования.
Переходит в MIT License 14 мая 2030 года.
---
## Контакты
- **Telegram**: [@ospab0](https://t.me/ospab0)
- **Email**: gvoprgrg@gmail.com

1
dnstt

@ -1 +0,0 @@
Subproject commit 0c5c52a57d899c05428c116898941761a2ed83c2

View File

@ -94,7 +94,7 @@ OSTP executes a Noise Protocol Framework exchange utilizing the `Noise_NNpsk0_25
2. The PSK is integrated into the state at pattern position zero, authorizing and encrypting the very first handshaking datagram. 2. The PSK is integrated into the state at pattern position zero, authorizing and encrypting the very first handshaking datagram.
3. Ephemeral Curve25519 key exchange is evaluated to synthesize autonomous symmetric keys for subsequent read/write channels. 3. Ephemeral Curve25519 key exchange is evaluated to synthesize autonomous symmetric keys for subsequent read/write channels.
The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a ±300-second synchronization window to accommodate clock drift and mobile roaming scenarios. The initial handshake payload includes a Unix timestamp to mitigate replay attacks. The server enforces a strict ±30-second synchronization window.
--- ---

View File

@ -1,102 +0,0 @@
# OSTP v0.3.1 Configuration Migration
In OSTP version 0.3.1, we have completely overhauled the `config.json` architecture for the client. The old monolithic structure (where all settings were in the root object) has been replaced by a modular system based on arrays of `inbounds` (incoming connections) and `outbounds` (outgoing connections), similar to Xray/V2Ray/Sing-box.
This allows OSTP to scale, support multiple proxy servers, multiple entry points (SOCKS5, TUN), and complex routing (`routing`).
## Automatic Migration
The `ostp` core includes a built-in automatic migrator. Upon starting any program (cli, gui, flutter), the core will check your `config.json`.
If the configuration lacks the `"version": "0.3.1"` field, OSTP will **automatically** convert your old config into the new modular format and save it to disk without data loss.
### What happens during migration:
1. **TUN and SOCKS5** -> converted into the `inbounds` array.
- The `socks5_bind` setting becomes an inbound `local_proxy` (SOCKS).
- The `tun` setting becomes an inbound `tun`.
2. **OSTP Server** -> moved into the `outbounds` array.
- Parameters `server`, `access_key`, `transport`, `mux` are combined into an outbound of type `"ostp"`.
3. **Split Tunneling (Exclude)** -> converted into `routing` rules.
- Old `domains` and `ips` are converted into rules routing traffic to the `"direct"` outbound.
- All other requests are routed by default to the `"proxy"` outbound.
4. **`version` fields**
- The field `"version": "0.3.1"` is added to prevent re-migration in the future. The `_comment` field has been removed.
## Change Example
### Before 0.3.1 (Old format)
```json
{
"mode": "client",
"log_level": "info",
"server": "1.2.3.4:50000",
"access_key": "secret",
"socks5_bind": "127.0.0.1:1088",
"tun": {
"enable": true
},
"exclude": {
"domains": ["localhost"]
}
}
```
### After 0.3.1 (New format)
```json
{
"mode": "client",
"version": "0.3.1",
"log": {
"level": "info"
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"auto_route": true,
"mtu": 1140
},
{
"type": "local_proxy",
"tag": "socks-in",
"protocol": "socks",
"listen": "127.0.0.1",
"port": 1088
}
],
"outbounds": [
{
"type": "ostp",
"tag": "proxy",
"server": "1.2.3.4",
"port": 50000,
"access_key": "secret",
"transport": {
"type": "udp"
}
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"routing": {
"rules": [
{
"domain_suffix": ["localhost"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
}
```
## Information for GUI Developers (ostp-gui, ostp-flutter)
If you are developing integrations or third-party clients, **you no longer need to parse the old fields**. You should use the `inbounds` and `outbounds` arrays. If the GUI passes a `serde_json::Value` to the core, the core will migrate it itself before starting. However, to save changes from the UI, you must modify the new array structure explicitly.

View File

@ -94,7 +94,7 @@ OSTP использует Noise Protocol Framework с паттерном `Noise_
2. PSK применяется на нулевой позиции паттерна, обеспечивая авторизацию и шифрование самой первой датаграммы рукопожатия (Zero-RTT авторизация). 2. PSK применяется на нулевой позиции паттерна, обеспечивая авторизацию и шифрование самой первой датаграммы рукопожатия (Zero-RTT авторизация).
3. Выполняется эфемерный обмен ключами Curve25519 для создания симметричных ключей передачи данных. 3. Выполняется эфемерный обмен ключами Curve25519 для создания симметричных ключей передачи данных.
Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер контролирует окно синхронизации (±300 секунд) с учётом дрейфа часов и смены сети при роуминге. Первичная полезная нагрузка рукопожатия содержит Unix-отметку времени для защиты от атак повторного воспроизведения (Replay Attacks). Сервер строго контролирует окно синхронизации (±30 секунд).
--- ---

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -9,7 +9,7 @@ anyhow.workspace = true
bytes.workspace = true bytes.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2" tracing-appender = "0.2"
ostp-core = { path = "../ostp-core" } ostp-core = { path = "../ostp-core" }
ostp-tun = { path = "../ostp-tun" } ostp-tun = { path = "../ostp-tun" }

View File

@ -18,7 +18,7 @@ impl Default for BridgeMetrics {
} }
} }
pub fn set_socket_protector<F>(_f: F) pub fn set_socket_protector<F>(f: F)
where where
F: Fn(i32) -> bool + Send + Sync + 'static, F: Fn(i32) -> bool + Send + Sync + 'static,
{ {

View File

@ -15,6 +15,8 @@ pub struct ClientConfig {
pub routing: RoutingConfig, pub routing: RoutingConfig,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub gui: Option<serde_json::Value>, pub gui: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api: Option<serde_json::Value>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -50,8 +52,6 @@ pub enum InboundConfig {
protocol: String, // "socks" or "http" protocol: String, // "socks" or "http"
listen: String, listen: String,
port: u16, port: u16,
#[serde(default)]
set_system_proxy: bool,
}, },
} }
@ -95,15 +95,7 @@ pub enum OutboundConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransportConfig { pub struct TransportConfig {
#[serde(default = "default_transport_mode")] #[serde(default = "default_transport_mode")]
pub r#type: String, // "udp", "uot", or "dns" pub r#type: String, // "udp" or "uot"
// Settings for DNS transport
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pubkey: Option<String>,
} }
fn default_transport_mode() -> String { "udp".to_string() } fn default_transport_mode() -> String { "udp".to_string() }
@ -112,9 +104,6 @@ impl Default for TransportConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
r#type: default_transport_mode(), r#type: default_transport_mode(),
domain: None,
resolver: None,
pubkey: None,
} }
} }
} }
@ -174,42 +163,33 @@ impl ClientConfig {
.with_context(|| format!("failed to parse JSON from {}", path.display()))?; .with_context(|| format!("failed to parse JSON from {}", path.display()))?;
let (migrated_json, was_migrated) = Self::migrate_json(raw_json); let (migrated_json, was_migrated) = Self::migrate_json(raw_json);
if was_migrated { if was_migrated {
tracing::warn!( tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display());
"Config at {} is in an outdated format. Run 'ostp --migrate' to upgrade it.", let serialized = serde_json::to_string_pretty(&migrated_json)?;
path.display() let header = "// OSTP Configuration v0.3.1\n// DO NOT EDIT THIS COMMENT - Migrator relies on it\n";
); let final_content = format!("{}{}", header, serialized);
std::fs::write(&path, final_content)
.with_context(|| format!("failed to save migrated config to {}", path.display()))?;
} }
let config: ClientConfig = serde_json::from_value(migrated_json) let config: ClientConfig = serde_json::from_value(migrated_json)
.with_context(|| format!("failed to deserialize config from {}", path.display()))?; .with_context(|| format!("failed to deserialize migrated config from {}", path.display()))?;
Ok(config) Ok(config)
} }
/// Migrates old monolithic JSON to the new modular format. /// Migrates old monolithic JSON to the new modular format.
/// Returns the migrated JSON value and a boolean indicating if a migration occurred. /// Returns the migrated JSON value and a boolean indicating if a migration occurred.
pub fn migrate_json(json: serde_json::Value) -> (serde_json::Value, bool) { pub fn migrate_json(mut json: serde_json::Value) -> (serde_json::Value, bool) {
// Consider the config already migrated if: let is_migrated = json.get("version").and_then(|v| v.as_str()) == Some("0.3.1");
// 1. Version matches exactly, OR if is_migrated {
// 2. The JSON already has the new modular format (inbounds + outbounds arrays)
let has_version = json.get("version").and_then(|v| v.as_str()) == Some(env!("CARGO_PKG_VERSION"));
let has_new_format = json.get("inbounds").and_then(|v| v.as_array()).is_some()
&& json.get("outbounds").and_then(|v| v.as_array()).is_some();
if has_version || has_new_format {
// If format is already new but version is old, just bump the version
if has_new_format && !has_version {
let mut updated = json.clone();
updated["version"] = serde_json::json!(env!("CARGO_PKG_VERSION"));
return (updated, false);
}
return (json, false); return (json, false);
} }
// Needs migration // Needs migration
let mut new_json = serde_json::json!({ let mut new_json = serde_json::json!({
"version": env!("CARGO_PKG_VERSION"), "version": "0.3.1",
}); });
// 1. Log level // 1. Log level
@ -325,6 +305,10 @@ impl ClientConfig {
new_json["gui"] = gui.clone(); new_json["gui"] = gui.clone();
} }
if let Some(api) = json.get("api") {
new_json["api"] = api.clone();
}
(new_json, true) (new_json, true)
} }
} }

View File

@ -1,41 +0,0 @@
use anyhow::{anyhow, Result};
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
use chacha20poly1305::aead::{Aead, KeyInit};
use sha2::{Sha256, Digest};
/// Symmetric IPC channel encryption for the tun-helper ↔ GUI pipe.
///
/// Both sides derive the same key from the per-launch random token, so no
/// secret is ever passed on the command line. The zero nonce is safe here
/// because each session uses a fresh random token, making key reuse impossible.
#[derive(Clone)]
pub struct IpcCrypto {
cipher: ChaCha20Poly1305,
}
impl IpcCrypto {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key)
.expect("32-byte key is always valid for ChaCha20Poly1305");
Self { cipher }
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.encrypt(nonce, plaintext)
.map_err(|e| anyhow!("IPC encrypt: {}", e))
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
let nonce = Nonce::from_slice(&[0u8; 12]);
self.cipher.decrypt(nonce, ciphertext)
.map_err(|e| anyhow!("IPC decrypt: {}", e))
}
}
/// Derive a 32-byte key from the per-session random token.
pub fn derive_key(token: &str) -> [u8; 32] {
let mut key = [0u8; 32];
key.copy_from_slice(&Sha256::digest(token.as_bytes()));
key
}

View File

@ -9,4 +9,3 @@ pub mod tunnel;
pub mod runner; pub mod runner;
pub mod logging; pub mod logging;
pub mod ipc_crypto;

View File

@ -73,21 +73,17 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) { if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) {
let (file_writer, guard) = tracing_appender::non_blocking(file); let (file_writer, guard) = tracing_appender::non_blocking(file);
let timer = tracing_subscriber::fmt::time::UtcTime::rfc_3339();
let fmt_layer = tracing_subscriber::fmt::layer() let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_line_number(false) .with_line_number(true)
.with_thread_ids(false) .with_thread_ids(false)
.with_thread_names(false) .with_thread_names(false)
.with_ansi(false) .with_ansi(false)
.with_timer(timer.clone())
.with_writer(file_writer); .with_writer(file_writer);
let stderr_layer = tracing_subscriber::fmt::layer() let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_timer(timer)
.with_writer(std::io::stderr); .with_writer(std::io::stderr);
let _ = tracing_subscriber::registry() let _ = tracing_subscriber::registry()
@ -111,7 +107,6 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracin
// Fallback: stderr only // Fallback: stderr only
let stderr_layer = tracing_subscriber::fmt::layer() let stderr_layer = tracing_subscriber::fmt::layer()
.with_target(true) .with_target(true)
.with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
.with_writer(std::io::stderr); .with_writer(std::io::stderr);
let _ = tracing_subscriber::registry() let _ = tracing_subscriber::registry()
.with(EnvFilter::new(level)) .with(EnvFilter::new(level))

View File

@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::watch; use tokio::sync::{mpsc, watch};
use crate::app::{BridgeCommand, ConnectionStatus, UiEvent};
use crate::config::{ClientConfig, InboundConfig}; use crate::config::{ClientConfig, InboundConfig};
use crate::tunnel::balancer::Balancer; use crate::tunnel::balancer::Balancer;
use crate::tunnel::outbounds::OutboundManager; use crate::tunnel::outbounds::OutboundManager;
@ -9,11 +10,11 @@ use crate::tunnel::router::Router;
pub async fn run_client_core( pub async fn run_client_core(
config: ClientConfig, config: ClientConfig,
_metrics: Arc<crate::bridge::BridgeMetrics>, metrics: Arc<crate::bridge::BridgeMetrics>,
mut shutdown_rx_ext: watch::Receiver<bool>, mut shutdown_rx_ext: watch::Receiver<bool>,
_config_rx: Option<watch::Receiver<ClientConfig>>, config_rx: Option<watch::Receiver<ClientConfig>>,
) -> Result<()> { ) -> Result<()> {
tracing::info!("starting client core"); println!("[ostp] Starting run_client_core with multi-server architecture");
let router = Arc::new(Router::new(config.routing.clone())); let router = Arc::new(Router::new(config.routing.clone()));
let balancer = Arc::new(Balancer::new(&config)); let balancer = Arc::new(Balancer::new(&config));

View File

@ -1 +0,0 @@
// Left empty by request

View File

@ -1,3 +1,4 @@
use std::sync::Arc; use std::sync::Arc;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use bytes::Bytes; use bytes::Bytes;
@ -8,11 +9,6 @@ pub enum Transport {
Uot { Uot {
tx: tokio::sync::mpsc::Sender<Bytes>, tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
},
Dnstt {
tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
_guard: Arc<tokio::sync::Mutex<ostp_core::dnstt::DnsttProcess>>,
} }
} }
@ -20,8 +16,8 @@ impl Transport {
pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> { pub async fn send(&self, frame: &Bytes) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.send(frame).await, Self::Udp(sock) => sock.send(frame).await,
Self::Uot { tx, .. } | Self::Dnstt { tx, .. } => { Self::Uot { tx, .. } => {
tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed"))?; tx.send(frame.clone()).await.map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed"))?;
Ok(frame.len()) Ok(frame.len())
} }
} }
@ -30,40 +26,31 @@ impl Transport {
pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> { pub async fn send_to(&self, frame: &Bytes, target: std::net::SocketAddr) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.send_to(frame, target).await, Self::Udp(sock) => sock.send_to(frame, target).await,
Self::Uot { .. } | Self::Dnstt { .. } => self.send(frame).await, Self::Uot { .. } => self.send(frame).await,
} }
} }
pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> { pub async fn recv(&self, buf: &mut [u8]) -> std::io::Result<usize> {
match self { match self {
Self::Udp(sock) => sock.recv(buf).await, Self::Udp(sock) => sock.recv(buf).await,
Self::Uot { rx, .. } | Self::Dnstt { rx, .. } => { Self::Uot { rx, .. } => {
let mut rx = rx.lock().await; let mut rx = rx.lock().await;
if let Some(frame) = rx.recv().await { match rx.recv().await {
let len = frame.len().min(buf.len()); Some(bytes) => {
buf[..len].copy_from_slice(&frame[..len]); let len = bytes.len().min(buf.len());
Ok(len) buf[..len].copy_from_slice(&bytes[..len]);
} else { Ok(len)
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "channel closed")) }
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed")),
} }
} }
} }
} }
pub async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, std::net::SocketAddr)> {
match self {
Self::Udp(sock) => sock.recv_from(buf).await,
Self::Uot { .. } | Self::Dnstt { .. } => {
let n = self.recv(buf).await?;
Ok((n, "127.0.0.1:0".parse().unwrap()))
}
}
}
pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> { pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
match self { match self {
Self::Udp(sock) => sock.local_addr(), Self::Udp(sock) => sock.local_addr(),
Self::Uot { .. } | Self::Dnstt { .. } => Ok("0.0.0.0:0".parse().unwrap()), Self::Uot { .. } => Ok("0.0.0.0:0".parse().unwrap()),
} }
} }
} }

View File

@ -1,5 +1,6 @@
use crate::config::{ClientConfig, OutboundConfig}; use crate::config::{ClientConfig, OutboundConfig};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
pub struct Balancer { pub struct Balancer {
outbounds: HashMap<String, OutboundConfig>, outbounds: HashMap<String, OutboundConfig>,
@ -59,7 +60,6 @@ impl Balancer {
/// Fetches the config for a concrete outbound /// Fetches the config for a concrete outbound
pub fn get_concrete_outbound(&self, tag: &str) -> Option<&OutboundConfig> { pub fn get_concrete_outbound(&self, tag: &str) -> Option<&OutboundConfig> {
let resolved_tag = self.resolve_outbound(tag); let resolved_tag = self.resolve_outbound(tag);
tracing::debug!("Balancer: tag '{}' resolved to '{}'", tag, resolved_tag);
self.outbounds.get(&resolved_tag) self.outbounds.get(&resolved_tag)
} }
} }

View File

@ -14,20 +14,13 @@ pub async fn run_socks_inbound(
outbound_manager: Arc<OutboundManager>, outbound_manager: Arc<OutboundManager>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
let InboundConfig::LocalProxy { tag, protocol, listen, port, set_system_proxy } = inbound_config else { let InboundConfig::LocalProxy { tag, protocol, listen, port } = inbound_config else {
return Err(anyhow!("Invalid config for LocalProxy inbound")); return Err(anyhow!("Invalid config for LocalProxy inbound"));
}; };
let bind_addr = format!("{}:{}", listen, port); let bind_addr = format!("{}:{}", listen, port);
tracing::info!("Starting {} proxy inbound on {} (tag: {})", protocol, bind_addr, tag); tracing::info!("Starting {} proxy inbound on {} (tag: {})", protocol, bind_addr, tag);
let _proxy_guard = if set_system_proxy {
let proxy_host = if listen == "0.0.0.0" { "127.0.0.1" } else { &listen };
Some(crate::sysproxy::SystemProxyGuard::enable(&format!("{}:{}", proxy_host, port)))
} else {
None
};
let listener = TcpListener::bind(&bind_addr).await?; let listener = TcpListener::bind(&bind_addr).await?;
loop { loop {
@ -92,7 +85,7 @@ async fn handle_socks5_connection(
} }
let atyp = buf[3]; let atyp = buf[3];
let (target_host, ip_addr) = match atyp { let (target_host, mut ip_addr) = match atyp {
0x01 => { // IPv4 0x01 => { // IPv4
stream.read_exact(&mut buf[0..4]).await?; stream.read_exact(&mut buf[0..4]).await?;
let ip = std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]); let ip = std::net::Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3]);

View File

@ -1,7 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::sync::Arc; use std::sync::Arc;
use crate::config::{ClientConfig, InboundConfig}; use crate::config::{ClientConfig, InboundConfig};
#[allow(unused_imports)]
use crate::tunnel::router::{Router, Session}; use crate::tunnel::router::{Router, Session};
use crate::tunnel::outbounds::OutboundManager; use crate::tunnel::outbounds::OutboundManager;
use tokio::sync::watch; use tokio::sync::watch;
@ -14,7 +13,7 @@ pub async fn run_tun_inbound(
outbound_manager: Arc<OutboundManager>, outbound_manager: Arc<OutboundManager>,
mut shutdown: watch::Receiver<bool>, mut shutdown: watch::Receiver<bool>,
) -> Result<()> { ) -> Result<()> {
use std::net::ToSocketAddrs;
use netstack_smoltcp::StackBuilder; use netstack_smoltcp::StackBuilder;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use futures::{StreamExt, SinkExt}; use futures::{StreamExt, SinkExt};
@ -73,7 +72,7 @@ pub async fn run_tun_inbound(
#[allow(unused_variables)] #[allow(unused_variables)]
let mut _route_guard = None; let mut _route_guard = None;
let (tun_to_stack, stack_to_tun) = { let (mut tun_to_stack, mut stack_to_tun) = {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
if let Some(fd) = fd { if let Some(fd) = fd {
@ -184,7 +183,7 @@ pub async fn run_tun_inbound(
let router_tcp = router.clone(); let router_tcp = router.clone();
let tag_tcp = tag.clone(); let tag_tcp = tag.clone();
let tcp_accept_task = tokio::spawn(async move { let mut tcp_accept_task = tokio::spawn(async move {
let Some(mut listener) = tcp_listener else { return; }; let Some(mut listener) = tcp_listener else { return; };
while let Some((mut stream, local, remote)) = listener.next().await { while let Some((mut stream, local, remote)) = listener.next().await {
let om = outbound_manager_tcp.clone(); let om = outbound_manager_tcp.clone();
@ -250,7 +249,7 @@ pub async fn run_tun_inbound(
let router_udp = router.clone(); let router_udp = router.clone();
let tag_udp = tag.clone(); let tag_udp = tag.clone();
let udp_proxy_task = tokio::spawn(async move { let mut udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket { if let Some(udp_sock) = udp_socket {
let (mut udp_rx, _udp_tx) = udp_sock.split(); let (mut udp_rx, _udp_tx) = udp_sock.split();
while let Some((payload, local, remote)) = udp_rx.next().await { while let Some((payload, local, remote)) = udp_rx.next().await {

View File

@ -66,7 +66,7 @@ pub fn bind_socket_to_interface(socket: &tokio::net::TcpSocket, _is_ipv6: bool,
Ok(()) Ok(())
} }
pub async fn dial_tcp(target_host: &str, target_port: u16, _phys_if_idx: Option<u32>) -> Result<TcpStream> { pub async fn dial_tcp(target_host: &str, target_port: u16, phys_if_idx: Option<u32>) -> Result<TcpStream> {
let addrs = tokio::net::lookup_host((target_host, target_port)).await?.collect::<Vec<_>>(); let addrs = tokio::net::lookup_host((target_host, target_port)).await?.collect::<Vec<_>>();
if addrs.is_empty() { if addrs.is_empty() {
return Err(anyhow!("Could not resolve target host: {}", target_host)); return Err(anyhow!("Could not resolve target host: {}", target_host));
@ -79,7 +79,7 @@ pub async fn dial_tcp(target_host: &str, target_port: u16, _phys_if_idx: Option<
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
if let Some(idx) = _phys_if_idx { if let Some(idx) = phys_if_idx {
if let Err(e) = bind_socket_to_interface(&socket, target_addr.is_ipv6(), idx) { if let Err(e) = bind_socket_to_interface(&socket, target_addr.is_ipv6(), idx) {
tracing::warn!("DIRECT: Failed to bind to physical interface {}: {}", idx, e); tracing::warn!("DIRECT: Failed to bind to physical interface {}: {}", idx, e);
} }

View File

@ -1,5 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::sync::Arc; use std::sync::Arc;
use tokio::net::TcpStream;
use crate::tunnel::balancer::Balancer; use crate::tunnel::balancer::Balancer;
use crate::config::OutboundConfig; use crate::config::OutboundConfig;
@ -11,7 +12,7 @@ pub mod socks;
pub struct OutboundManager { pub struct OutboundManager {
balancer: Arc<Balancer>, balancer: Arc<Balancer>,
phys_if_index: Option<u32>, phys_if_index: Option<u32>,
_phys_if_name: Option<String>, phys_if_name: Option<String>,
} }
impl OutboundManager { impl OutboundManager {
@ -23,7 +24,7 @@ impl OutboundManager {
Self { Self {
balancer, balancer,
phys_if_index, phys_if_index,
_phys_if_name: phys_if_name, phys_if_name,
} }
} }
@ -39,7 +40,7 @@ impl OutboundManager {
block::dial_tcp(target_host, target_port).await block::dial_tcp(target_host, target_port).await
} }
OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => { OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => {
ostp::dial_tcp(target_host, target_port, server, *port, access_key, transport, multiplex).await ostp::dial_tcp(server, *port, access_key, transport, multiplex).await
} }
OutboundConfig::Socks { server, port, .. } => { OutboundConfig::Socks { server, port, .. } => {
socks::dial_tcp(target_host, target_port, server, *port).await socks::dial_tcp(target_host, target_port, server, *port).await

View File

@ -1,233 +1,82 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::config::{TransportConfig, MultiplexConfig}; use crate::config::{TransportConfig, MultiplexConfig};
use ostp_core::{OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine}; use ostp_core::{NoiseRole, OstpEvent, ProtocolAction, ProtocolConfig, ProtocolMachine};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket;
/// Build the handshake payload the server expects:
/// [timestamp_u64_be (8 bytes)] [session_id_u32_be (4 bytes)] [access_key bytes]
fn build_handshake_payload(session_id: u32, access_key: &str) -> Vec<u8> {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut payload = Vec::with_capacity(12 + access_key.len());
payload.extend_from_slice(&ts.to_be_bytes());
payload.extend_from_slice(&session_id.to_be_bytes());
payload.extend_from_slice(access_key.as_bytes());
payload
}
/// Build a correctly configured ProtocolConfig for an outgoing OSTP connection.
fn make_initiator_config(
session_id: u32,
access_key: &str,
transport_cfg: &TransportConfig,
) -> ProtocolConfig {
let secrets = ostp_core::crypto::derive_all_secrets(access_key.as_bytes());
let payload = build_handshake_payload(session_id, access_key);
let mtu = match transport_cfg.r#type.as_str() {
"dns" => 1100,
_ => 1350,
};
// For DNS transport: use larger ack_delay and rto to match DNS round-trip latency
// (each DNS query + reply takes 300-800ms end-to-end through Cloudflare).
// For UDP: minimize ack_delay to 1ms (ACK asap) and let CC drive the RTO.
let (ack_delay_ms, rto_ms) = match transport_cfg.r#type.as_str() {
"dns" => (50, 1500),
_ => (1, 200),
};
ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
psk: secrets.psk,
session_id,
handshake_payload: payload,
max_padding: 256,
padding_strategy: ostp_core::framing::PaddingStrategy::Adaptive,
obfuscation_key: secrets.obfuscation_key,
max_reorder: 16384,
max_reorder_buffer: 8192,
ack_delay_ms,
rto_ms,
max_retries: 8,
max_sent_history: 32768,
handshake_pad_min: secrets.handshake_pad_min,
handshake_pad_max: secrets.handshake_pad_max,
mtu,
}
}
fn random_session_id() -> u32 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
std::time::Instant::now().hash(&mut h);
std::thread::current().id().hash(&mut h);
h.finish() as u32
}
pub async fn dial_tcp( pub async fn dial_tcp(
target_host: &str,
target_port: u16,
server: &str, server: &str,
port: u16, port: u16,
access_key: &str, access_key: &str,
transport_cfg: &TransportConfig, _transport: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<TcpStream> { ) -> Result<TcpStream> {
tracing::info!("Dialing OSTP server {}:{} for target {}:{}", server, port, target_host, target_port);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let local_addr = listener.local_addr()?; let local_addr = listener.local_addr()?;
let client_stream = tokio::net::TcpStream::connect(local_addr).await?; let client_stream = tokio::net::TcpStream::connect(local_addr).await?;
let (mut server_stream, _) = listener.accept().await?; let (mut server_stream, _) = listener.accept().await?;
let transport = make_transport(transport_cfg, server, port).await?; let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
let session_id = random_session_id();
let config = make_initiator_config(session_id, access_key, transport_cfg);
let mut machine = ProtocolMachine::new(config).unwrap();
let target_host_str = target_host.to_string();
let server_str = server.to_string();
// Spawn bridge task
tokio::spawn(async move {
// Send initial handshake
if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_action(action, &transport, &mut server_stream).await;
}
// Wait for handshake response (server sends HandshakePayload back) let mut psk = [0u8; 32];
let mut buf = [0u8; 8192]; let key_bytes = access_key.as_bytes();
let mut handshake_success = false; let len = key_bytes.len().min(32);
match tokio::time::timeout( psk[..len].copy_from_slice(&key_bytes[..len]);
std::time::Duration::from_millis(15000),
transport.recv(&mut buf), let config = ProtocolConfig {
).await { role: ostp_core::NoiseRole::Initiator,
Ok(Ok(n)) => { psk,
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) { session_id: 1,
handle_action(action, &transport, &mut server_stream).await; handshake_payload: vec![],
handshake_success = true; max_padding: 0,
} padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
} obfuscation_key: [0; 8],
_ => { max_reorder: 16384,
tracing::warn!("OSTP handshake timeout for {}:{}", server_str, port); max_reorder_buffer: 8192,
return; ack_delay_ms: 10,
} rto_ms: 100,
} max_retries: 5,
max_sent_history: 32768,
if !handshake_success { handshake_pad_min: 8,
tracing::warn!("TCP handshake failed or protocol machine error"); handshake_pad_max: 24,
return; mtu: 1400,
};
let mut machine = ProtocolMachine::new(config).unwrap();
// Spawn bridge task
tokio::spawn(async move {
if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_action(action, &udp, &mut server_stream).await;
} }
// Send connection request
let connect_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_host_str, target_port));
let connect_encoded = connect_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(connect_encoded))) {
handle_action(action, &transport, &mut server_stream).await;
}
// ── Wait for ConnectOk before forwarding any data ─────────────────
// This is critical: if we enter the data loop immediately, the TLS
// ClientHello arrives at the server before it has established the
// outbound TCP connection, causing it to drop the packet as
// "Relay DATA for unknown stream".
// The kernel will buffer incoming data from server_stream while we wait.
let mut connect_ok = false;
match tokio::time::timeout(
std::time::Duration::from_secs(30),
async {
let mut wait_buf = [0u8; 8192];
loop {
tokio::select! {
Ok(n) = transport.recv(&mut wait_buf) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(
bytes::Bytes::copy_from_slice(&wait_buf[..n]),
)) {
// Check for ConnectOk or Error before dispatching
let result = check_connect_result(&action);
handle_action(action, &transport, &mut server_stream).await;
match result {
Some(true) => return true,
Some(false) => return false,
None => {}
}
}
}
_ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &transport, &mut server_stream).await;
}
}
}
}
},
)
.await
{
Ok(true) => {
tracing::debug!("ConnectOk received for {}:{}, starting data forwarding", target_host_str, target_port);
connect_ok = true;
}
Ok(false) => {
tracing::warn!("Server refused connection to {}:{}", target_host_str, target_port);
}
Err(_) => {
tracing::warn!("ConnectOk timeout for {}:{}", target_host_str, target_port);
}
}
if !connect_ok {
return;
}
// ── Main bidirectional data forwarding loop ───────────────────────
// Backpressure: we track how many frames are in-flight vs the congestion
// window. When the window is full we stop reading from the TCP stream
// (the kernel buffers it) until the remote ACKs enough frames.
// This prevents overrunning the sender's sent_history and collapsing cwnd.
let mut buf = [0u8; 65535]; let mut buf = [0u8; 65535];
let mut udp_buf = [0u8; 65535]; let mut udp_buf = [0u8; 65535];
loop { loop {
// Compute adaptive tick interval:
// - If there is a pending ACK: tick = ack_delay (flush it quickly)
// - Otherwise: tick = rto/4 (check retransmits without busy-spinning)
// Floor at 1ms, ceiling at 50ms.
let tick_ms = (machine.rto().as_millis() / 4).clamp(1, 50) as u64;
let can_send = machine.in_flight_count() < machine.cwnd_packets().max(4);
tokio::select! { tokio::select! {
// Only read from the application TCP stream when cwnd allows Ok(n) = server_stream.read(&mut buf) => {
Ok(n) = server_stream.read(&mut buf), if can_send => {
if n == 0 { break; } if n == 0 { break; }
let data_msg = ostp_core::relay::RelayMessage::Data(buf[..n].to_vec()); if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::copy_from_slice(&buf[..n]))) {
let encoded = data_msg.encode(); handle_action(action, &udp, &mut server_stream).await;
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_action(action, &transport, &mut server_stream).await;
} }
} }
Ok(n) = transport.recv(&mut udp_buf) => { Ok(n) = udp.recv(&mut udp_buf) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&udp_buf[..n]))) { if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&udp_buf[..n]))) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &udp, &mut server_stream).await;
} }
} }
_ = tokio::time::sleep(std::time::Duration::from_millis(tick_ms)) => { _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
if let Ok(action) = machine.on_event(OstpEvent::Tick) { if let Ok(action) = machine.on_event(OstpEvent::Tick) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &udp, &mut server_stream).await;
} }
} }
} }
} }
}); });
Ok(client_stream) Ok(client_stream)
} }
@ -238,74 +87,71 @@ pub async fn handle_udp(
server: &str, server: &str,
port: u16, port: u16,
access_key: &str, access_key: &str,
transport_cfg: &TransportConfig, _transport: &TransportConfig,
_multiplex: &MultiplexConfig, _multiplex: &MultiplexConfig,
) -> Result<()> { ) -> Result<()> {
let transport = make_transport(transport_cfg, server, port).await?; let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
// Derive session_id from client source addr for stable per-flow sessions let mut psk = [0u8; 32];
let ip_bytes = match client_src.ip() { let key_bytes = access_key.as_bytes();
std::net::IpAddr::V4(v4) => { let len = key_bytes.len().min(32);
let o = v4.octets(); psk[..len].copy_from_slice(&key_bytes[..len]);
u32::from_be_bytes(o)
} let config = ProtocolConfig {
std::net::IpAddr::V6(v6) => { role: ostp_core::NoiseRole::Initiator,
let o = v6.octets(); psk,
u32::from_be_bytes([o[12], o[13], o[14], o[15]]) session_id: u32::from_ne_bytes([
} client_src.ip().to_string().as_bytes().get(0).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(1).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(2).copied().unwrap_or(0),
client_src.ip().to_string().as_bytes().get(3).copied().unwrap_or(0),
]),
handshake_payload: vec![],
max_padding: 0,
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
obfuscation_key: [0; 8],
max_reorder: 4096,
max_reorder_buffer: 2048,
ack_delay_ms: 50,
rto_ms: 200,
max_retries: 3,
max_sent_history: 8192,
handshake_pad_min: 8,
handshake_pad_max: 24,
mtu: 1400,
}; };
let session_id = ip_bytes ^ (client_src.port() as u32);
let config = make_initiator_config(session_id, access_key, transport_cfg);
let mut machine = ProtocolMachine::new(config)?; let mut machine = ProtocolMachine::new(config)?;
// Send handshake first // Send initial packet with UDP payload
if let Ok(action) = machine.on_event(OstpEvent::Start) { if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &udp).await;
} }
// Wait for handshake response (server sends HandshakePayload back) // Send the actual UDP payload
let mut buf = [0u8; 8192]; let relay_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_dst.ip(), target_dst.port()));
match tokio::time::timeout( let encoded = relay_msg.encode();
std::time::Duration::from_millis(15000),
transport.recv(&mut buf),
).await {
Ok(Ok(n)) => {
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
}
_ => {
tracing::warn!("OSTP handshake timeout for {}:{}", server, port);
return Ok(());
}
}
// Send relay UdpAssociate + data
let assoc_msg = ostp_core::relay::RelayMessage::UdpAssociate;
let encoded = assoc_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) { if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &udp).await;
} }
let data_msg = ostp_core::relay::RelayMessage::UdpData( // Send data packet
format!("{}:{}", target_dst.ip(), target_dst.port()), let data_msg = ostp_core::relay::RelayMessage::Data(payload.to_vec());
payload.to_vec()
);
let encoded = data_msg.encode(); let encoded = data_msg.encode();
if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) { if let Ok(action) = machine.on_event(OstpEvent::Outbound(1, bytes::Bytes::from(encoded))) {
handle_udp_action(action, &transport).await; handle_udp_action(action, &udp).await;
} }
// Keep-alive for a short time to receive response // Keep-alive for a short time to receive response
for _ in 0..5 { for _ in 0..5 {
let mut buf = [0u8; 8192];
match tokio::time::timeout( match tokio::time::timeout(
std::time::Duration::from_millis(100), std::time::Duration::from_millis(100),
transport.recv(&mut buf), udp.recv(&mut buf)
).await { ).await {
Ok(Ok(n)) => { Ok(Ok(n)) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) { let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
// Just process incoming UDP response internally
let _ = action;
}
} }
_ => break, _ => break,
} }
@ -314,85 +160,15 @@ pub async fn handle_udp(
Ok(()) Ok(())
} }
async fn make_transport( async fn handle_udp_action(action: ProtocolAction, udp: &UdpSocket) {
transport_cfg: &TransportConfig,
server: &str,
port: u16,
) -> Result<crate::transport::Transport> {
let debug = tracing::enabled!(tracing::Level::DEBUG);
match transport_cfg.r#type.as_str() {
"dns" => {
let domain = transport_cfg.domain.clone()
.unwrap_or_else(|| "tunnel.example.com".to_string());
let pubkey = transport_cfg.pubkey.clone()
.unwrap_or_else(|| "".to_string());
let resolver = transport_cfg.resolver.clone()
.unwrap_or_else(|| server.to_string());
let resolver_with_port = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let (local_port, process) = ostp_core::dnstt::spawn_client(&pubkey, &domain, &resolver_with_port, debug)?;
// Wait for dnstt-client to start its local TCP listener
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Connect TCP to the local dnstt-client port
let stream = tokio::net::TcpStream::connect(("127.0.0.1", local_port)).await?;
let (mut rh, mut wh) = stream.into_split();
let (tx_send, mut tx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
let (rx_send, rx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
// Writer task
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
while let Some(data) = tx_recv.recv().await {
let len = data.len() as u16;
if wh.write_u16(len).await.is_err() { break; }
if wh.write_all(&data).await.is_err() { break; }
}
});
// Reader task
tokio::spawn(async move {
use tokio::io::AsyncReadExt;
loop {
let len = match rh.read_u16().await {
Ok(l) => l,
Err(_) => break,
};
let mut buf = vec![0u8; len as usize];
if rh.read_exact(&mut buf).await.is_err() { break; }
if rx_send.send(bytes::Bytes::from(buf)).await.is_err() { break; }
}
});
Ok(crate::transport::Transport::Dnstt {
tx: tx_send,
rx: std::sync::Arc::new(tokio::sync::Mutex::new(rx_recv)),
_guard: std::sync::Arc::new(tokio::sync::Mutex::new(process)),
})
}
_ => {
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
udp.connect((server, port)).await?;
Ok(crate::transport::Transport::Udp(std::sync::Arc::new(udp)))
}
}
}
async fn handle_udp_action(action: ProtocolAction, transport: &crate::transport::Transport) {
match action { match action {
ProtocolAction::SendDatagram(data) => { ProtocolAction::SendDatagram(data) => {
let _ = transport.send(&data).await; let _ = udp.send(&data).await;
} }
ProtocolAction::Multiple(actions) => { ProtocolAction::Multiple(actions) => {
for a in actions { for a in actions {
if let ProtocolAction::SendDatagram(data) = a { if let ProtocolAction::SendDatagram(data) = a {
let _ = transport.send(&data).await; let _ = udp.send(&data).await;
} }
} }
} }
@ -400,59 +176,23 @@ async fn handle_udp_action(action: ProtocolAction, transport: &crate::transport:
} }
} }
async fn handle_action(action: ProtocolAction, transport: &crate::transport::Transport, server_stream: &mut tokio::net::TcpStream) { async fn handle_action(action: ProtocolAction, udp: &UdpSocket, server_stream: &mut tokio::net::TcpStream) {
match action { match action {
ProtocolAction::SendDatagram(data) => { ProtocolAction::SendDatagram(data) => {
let _ = transport.send(&data).await; let _ = udp.send(&data).await;
} }
ProtocolAction::DeliverApp(_stream_id, payload) => { ProtocolAction::DeliverApp(_stream_id, payload) => {
if let Ok(msg) = ostp_core::relay::RelayMessage::decode(&payload) { let _ = server_stream.write_all(&payload).await;
match msg {
ostp_core::relay::RelayMessage::Data(data) => {
let _ = server_stream.write_all(&data).await;
}
ostp_core::relay::RelayMessage::ConnectOk => {
tracing::debug!("TCP Connection established successfully");
}
ostp_core::relay::RelayMessage::Error(err) => {
tracing::warn!("Server returned TCP connection error: {}", err);
}
_ => {}
}
}
} }
ProtocolAction::Multiple(actions) => { ProtocolAction::Multiple(actions) => {
for a in actions { for a in actions {
Box::pin(handle_action(a, transport, server_stream)).await; match a {
ProtocolAction::SendDatagram(data) => { let _ = udp.send(&data).await; }
ProtocolAction::DeliverApp(_stream_id, payload) => { let _ = server_stream.write_all(&payload).await; }
_ => {}
}
} }
} }
_ => {} _ => {}
} }
} }
/// Inspect a ProtocolAction for ConnectOk / Error relay messages.
/// Returns Some(true) on ConnectOk, Some(false) on Error, None if neither.
/// Works recursively through Multiple actions.
fn check_connect_result(action: &ProtocolAction) -> Option<bool> {
match action {
ProtocolAction::DeliverApp(_stream_id, payload) => {
if let Ok(msg) = ostp_core::relay::RelayMessage::decode(payload) {
match msg {
ostp_core::relay::RelayMessage::ConnectOk => return Some(true),
ostp_core::relay::RelayMessage::Error(_) => return Some(false),
_ => {}
}
}
None
}
ProtocolAction::Multiple(actions) => {
for a in actions {
if let Some(result) = check_connect_result(a) {
return Some(result);
}
}
None
}
_ => None,
}
}

View File

@ -126,6 +126,7 @@ pub fn get_process_name_from_port(port: u16) -> Option<String> {
use std::fs; use std::fs;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
let mut target_inode = None;
let hex_port = format!("{:04X}", port); let hex_port = format!("{:04X}", port);
let check_net_file = |path: &str| -> Option<u64> { let check_net_file = |path: &str| -> Option<u64> {
@ -145,11 +146,12 @@ pub fn get_process_name_from_port(port: u16) -> Option<String> {
None None
}; };
let target_inode = check_net_file("/proc/net/tcp") target_inode = check_net_file("/proc/net/tcp")
.or_else(|| check_net_file("/proc/net/tcp6")) .or_else(|| check_net_file("/proc/net/tcp6"))
.or_else(|| check_net_file("/proc/net/udp")) .or_else(|| check_net_file("/proc/net/udp"))
.or_else(|| check_net_file("/proc/net/udp6"))?; .or_else(|| check_net_file("/proc/net/udp6"));
let target_inode = target_inode?;
let socket_str = format!("socket:[{}]", target_inode); let socket_str = format!("socket:[{}]", target_inode);
for entry in fs::read_dir("/proc").ok()?.filter_map(Result::ok) { for entry in fs::read_dir("/proc").ok()?.filter_map(Result::ok) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ostp-control</title>
<script type="module" crossorigin src="./assets/index-eeBKspfZ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DADo1Z55.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -12,10 +12,7 @@ rand.workspace = true
snow.workspace = true snow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
byteorder = "1.5"
sha2.workspace = true sha2.workspace = true
hmac.workspace = true hmac.workspace = true
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
hkdf = "0.12.0" hkdf = "0.12.0"
tokio.workspace = true
serde = { version = "1.0", features = ["derive"] }

View File

@ -4,12 +4,6 @@
//! bandwidth and minimum RTT to determine the optimal sending rate. //! bandwidth and minimum RTT to determine the optimal sending rate.
//! This replaces the fixed `retransmit_budget = 8` with an adaptive //! This replaces the fixed `retransmit_budget = 8` with an adaptive
//! congestion window that responds to network conditions. //! 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}; use std::time::{Duration, Instant};
@ -21,14 +15,8 @@ pub struct CongestionController {
ssthresh: u64, ssthresh: u64,
/// Current phase /// Current phase
phase: Phase, phase: Phase,
/// Minimum RTT observed (for BBR-style bandwidth estimation) /// Minimum RTT observed
min_rtt: Duration, 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 currently in flight (unacknowledged)
bytes_in_flight: u64, bytes_in_flight: u64,
/// Total bytes acknowledged (for bandwidth estimation) /// Total bytes acknowledged (for bandwidth estimation)
@ -49,43 +37,31 @@ pub struct CongestionController {
enum Phase { enum Phase {
/// Exponential growth until loss or ssthresh /// Exponential growth until loss or ssthresh
SlowStart, SlowStart,
/// Probe bandwidth: additive increase /// Probe bandwidth: cycle through pacing gains
ProbeBandwidth, ProbeBandwidth,
} }
/// Initial congestion window: 32 packets × MTU (IW10 is too conservative for modern links) /// Initial congestion window: 10 packets × MTU
const INITIAL_CWND_PACKETS: u64 = 32; const INITIAL_CWND_PACKETS: u64 = 10;
/// Minimum cwnd: 2 packets /// Minimum cwnd: 2 packets
const MIN_CWND_PACKETS: u64 = 2; const MIN_CWND_PACKETS: u64 = 2;
/// Min RTT expiry window (after which we re-probe) /// Min RTT expiry window (after which we re-probe)
const MIN_RTT_EXPIRY: Duration = Duration::from_secs(10); 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 { impl CongestionController {
pub fn new(mtu: u64) -> Self { pub fn new(mtu: u64) -> Self {
let now = Instant::now(); let now = Instant::now();
let initial_cwnd = INITIAL_CWND_PACKETS * mtu; 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 { Self {
cwnd: initial_cwnd, cwnd: initial_cwnd,
ssthresh: u64::MAX, ssthresh: u64::MAX,
phase: Phase::SlowStart, phase: Phase::SlowStart,
min_rtt: INITIAL_RTT, min_rtt: Duration::from_millis(100), // Conservative initial estimate
srtt: INITIAL_RTT,
rttvar: INITIAL_RTT / 2,
rtt_initialized: false,
bytes_in_flight: 0, bytes_in_flight: 0,
total_acked: 0, total_acked: 0,
last_ack_time: now, last_ack_time: now,
loss_count: 0, loss_count: 0,
pacing_rate: initial_pacing, pacing_rate: initial_cwnd * 10, // initial: ~10 windows/sec
mtu, mtu,
min_rtt_stamp: now, min_rtt_stamp: now,
} }
@ -106,20 +82,9 @@ impl CongestionController {
self.pacing_rate self.pacing_rate
} }
/// Returns the smoothed RTT estimate (SRTT). /// Returns the smoothed RTT estimate.
pub fn smoothed_rtt(&self) -> Duration { pub fn smoothed_rtt(&self) -> Duration {
self.srtt self.min_rtt
}
/// 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. /// Returns how many bytes can still be sent.
@ -150,13 +115,16 @@ impl CongestionController {
self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes); self.bytes_in_flight = self.bytes_in_flight.saturating_sub(bytes);
self.total_acked = self.total_acked.saturating_add(bytes); self.total_acked = self.total_acked.saturating_add(bytes);
// Update RTT measurements // Update RTT
self.update_rtt(rtt, now); self.update_rtt(rtt, now);
// Update bandwidth estimate
self.update_bandwidth(bytes, now);
// State machine // State machine
match self.phase { match self.phase {
Phase::SlowStart => { Phase::SlowStart => {
// Exponential growth: increase cwnd by acked bytes (doubles per RTT) // Exponential growth: increase cwnd by acked bytes
self.cwnd = self.cwnd.saturating_add(bytes); self.cwnd = self.cwnd.saturating_add(bytes);
if self.cwnd >= self.ssthresh { if self.cwnd >= self.ssthresh {
self.phase = Phase::ProbeBandwidth; self.phase = Phase::ProbeBandwidth;
@ -196,49 +164,32 @@ impl CongestionController {
self.update_pacing_rate(); 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 ────────────────────────────────────────────────────────────── // ── Private ──────────────────────────────────────────────────────────────
fn update_rtt(&mut self, rtt: Duration, now: Instant) { fn update_rtt(&mut self, rtt: Duration, now: Instant) {
// Update windowed minimum RTT (for pacing) // Track windowed minimum RTT
if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY { if rtt < self.min_rtt || now.duration_since(self.min_rtt_stamp) >= MIN_RTT_EXPIRY {
self.min_rtt = rtt; self.min_rtt = rtt;
self.min_rtt_stamp = now; self.min_rtt_stamp = now;
} }
// 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_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
}
}
fn update_pacing_rate(&mut self) { fn update_pacing_rate(&mut self) {
// Pacing rate = cwnd / min_rtt (delivery rate target) // Pacing rate = cwnd / min_rtt (with gain)
let rtt_us = self.min_rtt.as_micros().max(1) as u64; let rtt_us = self.min_rtt.as_micros().max(1) as u64;
self.pacing_rate = self.cwnd * 1_000_000 / rtt_us; self.pacing_rate = self.cwnd * 1_000_000 / rtt_us;
} }
@ -251,18 +202,19 @@ mod tests {
#[test] #[test]
fn test_initial_state() { fn test_initial_state() {
let cc = CongestionController::new(1200); let cc = CongestionController::new(1200);
assert_eq!(cc.cwnd(), 32 * 1200); // 32 * 1200 assert_eq!(cc.cwnd(), 12000); // 10 * 1200
assert!(cc.can_send()); assert!(cc.can_send());
assert_eq!(cc.cwnd_packets(), 32); assert_eq!(cc.cwnd_packets(), 10);
} }
#[test] #[test]
fn test_slow_start_growth() { fn test_slow_start_growth() {
let mut cc = CongestionController::new(1200); let mut cc = CongestionController::new(1200);
let initial = cc.cwnd(); // Simulate sending and ACKing
cc.on_send(1200); cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(50)); cc.on_ack(1200, Duration::from_millis(50));
assert!(cc.cwnd() > initial); // cwnd should grow
assert!(cc.cwnd() > 12000);
} }
#[test] #[test]
@ -277,7 +229,7 @@ mod tests {
fn test_can_send_limits() { fn test_can_send_limits() {
let mut cc = CongestionController::new(1200); let mut cc = CongestionController::new(1200);
// Send until cwnd is exhausted // Send until cwnd is exhausted
for _ in 0..32 { for _ in 0..10 {
cc.on_send(1200); cc.on_send(1200);
} }
assert!(!cc.can_send()); // cwnd exhausted assert!(!cc.can_send()); // cwnd exhausted
@ -292,46 +244,10 @@ mod tests {
} }
#[test] #[test]
fn test_rtt_tracking_first_sample() { fn test_rtt_tracking() {
let mut cc = CongestionController::new(1200); let mut cc = CongestionController::new(1200);
cc.on_send(1200); cc.on_send(1200);
cc.on_ack(1200, Duration::from_millis(25)); cc.on_ack(1200, Duration::from_millis(25));
// After first sample: SRTT = 25ms, RTTVAR = 12ms
assert_eq!(cc.smoothed_rtt(), Duration::from_millis(25)); 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);
}
} }

View File

@ -1,417 +0,0 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read};
const BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
/// Encodes a byte slice into Base32 (RFC 4648) without padding, lowercase.
pub fn base32_encode(data: &[u8]) -> String {
let mut result = String::with_capacity((data.len() * 8 + 4) / 5);
let mut buffer = 0u32;
let mut bits_left = 0;
for &b in data {
buffer = (buffer << 8) | (b as u32);
bits_left += 8;
while bits_left >= 5 {
bits_left -= 5;
let index = ((buffer >> bits_left) & 0x1F) as usize;
result.push(BASE32_ALPHABET[index] as char);
}
}
if bits_left > 0 {
let index = ((buffer << (5 - bits_left)) & 0x1F) as usize;
result.push(BASE32_ALPHABET[index] as char);
}
result
}
/// Decodes a Base32 string (case-insensitive, no padding) into a byte vector.
pub fn base32_decode(encoded: &str) -> Option<Vec<u8>> {
let mut result = Vec::with_capacity(encoded.len() * 5 / 8);
let mut buffer = 0u32;
let mut bits_left = 0;
for c in encoded.bytes() {
let val = match c {
b'a'..=b'z' => c - b'a',
b'A'..=b'Z' => c - b'A',
b'2'..=b'7' => c - b'2' + 26,
_ => return None, // Invalid character
};
buffer = (buffer << 5) | (val as u32);
bits_left += 5;
if bits_left >= 8 {
bits_left -= 8;
result.push((buffer >> bits_left) as u8);
}
}
Some(result)
}
#[derive(Debug, Clone, PartialEq)]
pub enum DnsRecordType {
A,
CNAME,
NULL,
TXT,
AAAA,
Unknown(u16),
}
impl From<u16> for DnsRecordType {
fn from(val: u16) -> Self {
match val {
1 => DnsRecordType::A,
5 => DnsRecordType::CNAME,
10 => DnsRecordType::NULL,
16 => DnsRecordType::TXT,
28 => DnsRecordType::AAAA,
_ => DnsRecordType::Unknown(val),
}
}
}
impl DnsRecordType {
pub fn as_u16(&self) -> u16 {
match self {
DnsRecordType::A => 1,
DnsRecordType::CNAME => 5,
DnsRecordType::NULL => 10,
DnsRecordType::TXT => 16,
DnsRecordType::AAAA => 28,
DnsRecordType::Unknown(v) => *v,
}
}
}
#[derive(Debug, Clone)]
pub struct DnsQuestion {
pub name: String,
pub qtype: DnsRecordType,
pub qclass: u16, // Usually 1 (IN)
}
#[derive(Debug, Clone)]
pub struct DnsAnswer {
pub name: String,
pub rtype: DnsRecordType,
pub rclass: u16,
pub ttl: u32,
pub rdata: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct DnsPacket {
pub id: u16,
pub flags: u16,
pub questions: Vec<DnsQuestion>,
pub answers: Vec<DnsAnswer>,
}
impl DnsPacket {
pub fn new_query(id: u16, name: &str, qtype: DnsRecordType) -> Self {
DnsPacket {
id,
flags: 0x0100, // Standard query, recursion desired
questions: vec![DnsQuestion {
name: name.to_string(),
qtype,
qclass: 1, // IN
}],
answers: vec![],
}
}
pub fn new_response(id: u16, name: &str, rtype: DnsRecordType, rdata: Vec<u8>) -> Self {
DnsPacket {
id,
flags: 0x8180, // Response, standard query, recursion desired, recursion available
questions: vec![DnsQuestion {
name: name.to_string(),
qtype: rtype.clone(),
qclass: 1, // IN
}],
answers: if rdata.is_empty() {
vec![]
} else {
vec![DnsAnswer {
name: name.to_string(),
rtype,
rclass: 1,
ttl: 0, // No caching
rdata,
}]
},
}
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
let _ = buf.write_u16::<BigEndian>(self.id);
let _ = buf.write_u16::<BigEndian>(self.flags);
let _ = buf.write_u16::<BigEndian>(self.questions.len() as u16);
let _ = buf.write_u16::<BigEndian>(self.answers.len() as u16);
let _ = buf.write_u16::<BigEndian>(0); // Authority PR
let _ = buf.write_u16::<BigEndian>(0); // Additional PR
for q in &self.questions {
encode_domain_name(&mut buf, &q.name);
let _ = buf.write_u16::<BigEndian>(q.qtype.as_u16());
let _ = buf.write_u16::<BigEndian>(q.qclass);
}
for a in &self.answers {
encode_domain_name(&mut buf, &a.name);
let _ = buf.write_u16::<BigEndian>(a.rtype.as_u16());
let _ = buf.write_u16::<BigEndian>(a.rclass);
let _ = buf.write_u32::<BigEndian>(a.ttl);
if a.rtype == DnsRecordType::TXT {
// TXT records have character-strings length-prefixed
// We split into chunks of up to 255 bytes
let mut txt_data = Vec::new();
for chunk in a.rdata.chunks(255) {
txt_data.push(chunk.len() as u8);
txt_data.extend_from_slice(chunk);
}
let _ = buf.write_u16::<BigEndian>(txt_data.len() as u16);
buf.extend_from_slice(&txt_data);
} else {
let _ = buf.write_u16::<BigEndian>(a.rdata.len() as u16);
buf.extend_from_slice(&a.rdata);
}
}
buf
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() < 12 {
return None;
}
let mut cursor = Cursor::new(data);
let id = cursor.read_u16::<BigEndian>().ok()?;
let flags = cursor.read_u16::<BigEndian>().ok()?;
let qdcount = cursor.read_u16::<BigEndian>().ok()?;
let ancount = cursor.read_u16::<BigEndian>().ok()?;
let _nscount = cursor.read_u16::<BigEndian>().ok()?;
let _arcount = cursor.read_u16::<BigEndian>().ok()?;
let mut questions = Vec::new();
for _ in 0..qdcount {
let name = decode_domain_name(&mut cursor, data)?;
let qtype = cursor.read_u16::<BigEndian>().ok()?.into();
let qclass = cursor.read_u16::<BigEndian>().ok()?;
questions.push(DnsQuestion { name, qtype, qclass });
}
let mut answers = Vec::new();
for _ in 0..ancount {
let name = decode_domain_name(&mut cursor, data)?;
let rtype: DnsRecordType = cursor.read_u16::<BigEndian>().ok()?.into();
let rclass = cursor.read_u16::<BigEndian>().ok()?;
let ttl = cursor.read_u32::<BigEndian>().ok()?;
let rdlength = cursor.read_u16::<BigEndian>().ok()?;
let mut rdata = vec![0u8; rdlength as usize];
cursor.read_exact(&mut rdata).ok()?;
if rtype == DnsRecordType::TXT {
// Decode TXT string chunks
let mut decoded_txt = Vec::new();
let mut txt_cursor = Cursor::new(&rdata);
while txt_cursor.position() < rdata.len() as u64 {
if let Ok(len) = txt_cursor.read_u8() {
let mut chunk = vec![0u8; len as usize];
if txt_cursor.read_exact(&mut chunk).is_ok() {
decoded_txt.extend_from_slice(&chunk);
} else {
break;
}
} else {
break;
}
}
rdata = decoded_txt;
}
answers.push(DnsAnswer {
name,
rtype,
rclass,
ttl,
rdata,
});
}
// Skip authority and additional sections (not needed for basic payload extraction)
Some(DnsPacket {
id,
flags,
questions,
answers,
})
}
}
fn encode_domain_name(buf: &mut Vec<u8>, name: &str) {
for part in name.split('.') {
if part.is_empty() {
continue;
}
let len = part.len().min(63) as u8;
buf.push(len);
buf.extend_from_slice(&part.as_bytes()[..len as usize]);
}
buf.push(0); // Root label
}
fn decode_domain_name(cursor: &mut Cursor<&[u8]>, full_data: &[u8]) -> Option<String> {
let mut parts = Vec::new();
let mut jumps = 0;
let mut current_pos = cursor.position();
loop {
if jumps > 100 {
return None; // Prevent infinite loops
}
if current_pos >= full_data.len() as u64 {
return None;
}
let len = full_data[current_pos as usize];
if len == 0 {
if jumps == 0 {
cursor.set_position(current_pos + 1);
}
break;
}
if len & 0xC0 == 0xC0 {
// Pointer
if current_pos + 1 >= full_data.len() as u64 {
return None;
}
let pointer = (((len & 0x3F) as u16) << 8) | (full_data[current_pos as usize + 1] as u16);
if jumps == 0 {
cursor.set_position(current_pos + 2);
}
jumps += 1;
current_pos = pointer as u64;
continue;
}
current_pos += 1;
if current_pos + len as u64 > full_data.len() as u64 {
return None;
}
let part = &full_data[current_pos as usize..(current_pos + len as u64) as usize];
parts.push(String::from_utf8_lossy(part).into_owned());
current_pos += len as u64;
if jumps == 0 {
cursor.set_position(current_pos);
}
}
if parts.is_empty() {
Some(".".to_string())
} else {
Some(parts.join("."))
}
}
/// Encodes a payload into a list of subdomain labels and appends the base domain.
/// Each label is max 63 chars. The base32 string is chunked.
pub fn encode_payload_to_domain(payload: &[u8], base_domain: &str) -> String {
let encoded = base32_encode(payload);
let mut domain = String::new();
let mut start = 0;
while start < encoded.len() {
let end = (start + 63).min(encoded.len());
domain.push_str(&encoded[start..end]);
domain.push('.');
start = end;
}
domain.push_str(base_domain);
domain
}
/// Decodes a payload from a subdomain string, ignoring the base domain.
pub fn decode_domain_to_payload(full_domain: &str, base_domain: &str) -> Option<Vec<u8>> {
// Strip base domain and trailing dots
let stripped = full_domain
.trim_end_matches('.')
.strip_suffix(base_domain)?;
let stripped = stripped.trim_end_matches('.');
let mut base32_str = String::with_capacity(stripped.len());
for part in stripped.split('.') {
base32_str.push_str(part);
}
base32_decode(&base32_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base32() {
let data = b"Hello, OSTP DNS Tunnel!";
let encoded = base32_encode(data);
let decoded = base32_decode(&encoded).unwrap();
assert_eq!(data.as_ref(), decoded.as_slice());
}
#[test]
fn test_domain_encoding() {
let payload = vec![0x12; 20];
let base_domain = "tunnel.example.com";
let domain = encode_payload_to_domain(&payload, base_domain);
// Ensure no label is > 63 chars
for part in domain.split('.') {
assert!(part.len() <= 63);
}
assert!(domain.ends_with(base_domain));
let decoded = decode_domain_to_payload(&domain, base_domain).unwrap();
assert_eq!(payload, decoded);
}
#[test]
fn test_dns_packet() {
let payload = vec![1, 2, 3, 4, 5];
let domain = encode_payload_to_domain(&payload, "t.com");
let query = DnsPacket::new_query(1234, &domain, DnsRecordType::TXT);
let encoded_query = query.encode();
let decoded_query = DnsPacket::decode(&encoded_query).unwrap();
assert_eq!(decoded_query.id, 1234);
assert_eq!(decoded_query.questions[0].name, domain);
assert_eq!(decoded_query.questions[0].qtype, DnsRecordType::TXT);
let response_data = vec![5, 4, 3, 2, 1];
let response = DnsPacket::new_response(1234, &domain, DnsRecordType::TXT, response_data.clone());
let encoded_resp = response.encode();
let decoded_resp = DnsPacket::decode(&encoded_resp).unwrap();
assert_eq!(decoded_resp.answers[0].rdata, response_data);
}
}

View File

@ -1,94 +0,0 @@
use std::time::Duration;
use tokio::time::Instant;
use crate::dns::{DnsPacket, DnsRecordType, encode_payload_to_domain};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct DnsProbeResult {
pub name: String,
pub ip: String,
pub latency_ms: Option<u64>,
}
const PUBLIC_DNS_SERVERS: &[(&str, &str)] = &[
("Cloudflare", "1.1.1.1"),
("Cloudflare2", "1.0.0.1"),
("Google", "8.8.8.8"),
("Google2", "8.8.4.4"),
("Quad9", "9.9.9.9"),
("AdGuard", "94.140.14.14"),
("Yandex", "77.88.8.8"),
("Yandex2", "77.88.8.1"),
("SkyDNS", "193.58.251.251"),
("AliDNS", "223.5.5.5"),
("Tencent", "119.29.29.29"),
("114DNS", "114.114.114.114"),
("Shecan", "178.22.122.100"),
("Electro", "78.157.42.100"),
("Begzar", "185.55.226.26"),
];
async fn probe_resolver(domain: &str, resolver_ip: &str) -> Option<u64> {
let (probe_bytes, id) = {
let mut rng = rand::thread_rng();
let probe_bytes: [u8; 4] = rng.gen();
let id: u16 = rng.gen();
(probe_bytes, id)
};
let fqdn = encode_payload_to_domain(&probe_bytes, domain);
let qtype = if rand::thread_rng().gen_bool(0.5) { DnsRecordType::TXT } else { DnsRecordType::NULL };
let packet = DnsPacket::new_query(id, &fqdn, qtype);
let encoded = packet.encode();
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
sock.connect(format!("{}:53", resolver_ip)).await.ok()?;
let start = Instant::now();
sock.send(&encoded).await.ok()?;
let mut buf = [0u8; 4096];
match tokio::time::timeout(Duration::from_secs(2), sock.recv(&mut buf)).await {
Ok(Ok(n)) => {
if let Some(resp) = DnsPacket::decode(&buf[..n]) {
// Check if RCODE == 0 (NOERROR) and it has answers
let rcode = resp.flags & 0x000F;
if rcode == 0 && !resp.answers.is_empty() {
return Some(start.elapsed().as_millis() as u64);
}
}
None
},
_ => None,
}
}
pub async fn run_dns_prober(domain: &str) -> Result<Vec<DnsProbeResult>, String> {
if domain.is_empty() {
return Err("Please enter the tunnel domain first (e.g. tunnel.myvpn.com)".into());
}
let tasks: Vec<_> = PUBLIC_DNS_SERVERS
.iter()
.map(|(name, ip)| {
let domain = domain.to_string();
let name = name.to_string();
let ip = ip.to_string();
tokio::spawn(async move {
let latency_ms = probe_resolver(&domain, &ip).await;
DnsProbeResult { name, ip, latency_ms }
})
})
.collect();
let mut results = Vec::with_capacity(tasks.len());
for task in tasks {
if let Ok(r) = task.await {
results.push(r);
}
}
results.sort_by_key(|r| r.latency_ms.unwrap_or(u64::MAX));
Ok(results)
}

View File

@ -4,9 +4,6 @@ pub mod framing;
pub mod protocol; pub mod protocol;
pub mod relay; pub mod relay;
pub mod resumption; pub mod resumption;
pub mod dns;
pub mod dns_prober;
pub mod dnstt;
pub use crypto::NoiseRole; pub use crypto::NoiseRole;
pub use framing::{TrafficProfile, PaddingStrategy}; pub use framing::{TrafficProfile, PaddingStrategy};

View File

@ -2,7 +2,7 @@ use bytes::Bytes;
use rand::Rng; use rand::Rng;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use thiserror::Error; use thiserror::Error;
use std::collections::BTreeMap; use std::collections::{BTreeMap, VecDeque};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::congestion::CongestionController; use crate::congestion::CongestionController;
@ -75,7 +75,7 @@ pub struct ProtocolMachine {
send_nonce: u64, send_nonce: u64,
expected_recv_nonce: u64, expected_recv_nonce: u64,
reorder_buffer: BTreeMap<u64, ProtocolAction>, reorder_buffer: BTreeMap<u64, ProtocolAction>,
sent_history: BTreeMap<u64, SentFrame>, sent_history: VecDeque<SentFrame>,
session_id: u32, session_id: u32,
handshake_payload: Vec<u8>, handshake_payload: Vec<u8>,
padder: AdaptivePadder, padder: AdaptivePadder,
@ -83,8 +83,7 @@ pub struct ProtocolMachine {
max_reorder: u64, max_reorder: u64,
max_reorder_buffer: usize, max_reorder_buffer: usize,
ack_delay: Duration, ack_delay: Duration,
/// Initial/fallback RTO from config (overridden by cc.rto() after first RTT sample) rto: Duration,
rto_initial: Duration,
max_retries: u8, max_retries: u8,
max_sent_history: usize, max_sent_history: usize,
ack_pending: bool, ack_pending: bool,
@ -101,11 +100,11 @@ pub struct ProtocolMachine {
/// Key-derived handshake padding range /// Key-derived handshake padding range
handshake_pad_min: usize, handshake_pad_min: usize,
handshake_pad_max: usize, handshake_pad_max: usize,
_mtu: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SentFrame { struct SentFrame {
#[allow(dead_code)] // mirrored in BTreeMap key; kept for Debug output
nonce: u64, nonce: u64,
bytes: Bytes, bytes: Bytes,
last_sent: Instant, last_sent: Instant,
@ -129,7 +128,7 @@ impl ProtocolMachine {
send_nonce: 0, send_nonce: 0,
expected_recv_nonce: 0, expected_recv_nonce: 0,
reorder_buffer: BTreeMap::new(), reorder_buffer: BTreeMap::new(),
sent_history: BTreeMap::new(), sent_history: VecDeque::with_capacity(config.max_sent_history.max(1)),
session_id: config.session_id, session_id: config.session_id,
handshake_payload: config.handshake_payload, handshake_payload: config.handshake_payload,
padder: AdaptivePadder::new(config.mtu, config.max_padding, config.padding_strategy), padder: AdaptivePadder::new(config.mtu, config.max_padding, config.padding_strategy),
@ -137,7 +136,7 @@ impl ProtocolMachine {
max_reorder: config.max_reorder.max(1), max_reorder: config.max_reorder.max(1),
max_reorder_buffer: config.max_reorder_buffer.max(1), max_reorder_buffer: config.max_reorder_buffer.max(1),
ack_delay: Duration::from_millis(config.ack_delay_ms.max(1)), ack_delay: Duration::from_millis(config.ack_delay_ms.max(1)),
rto_initial: Duration::from_millis(config.rto_ms.max(1)), rto: Duration::from_millis(config.rto_ms.max(1)),
max_retries: config.max_retries.max(1), max_retries: config.max_retries.max(1),
max_sent_history: config.max_sent_history.max(1), max_sent_history: config.max_sent_history.max(1),
ack_pending: false, ack_pending: false,
@ -147,25 +146,20 @@ impl ProtocolMachine {
cc: CongestionController::new(config.mtu as u64), cc: CongestionController::new(config.mtu as u64),
handshake_pad_min: config.handshake_pad_min.max(8), handshake_pad_min: config.handshake_pad_min.max(8),
handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16), handshake_pad_max: config.handshake_pad_max.max(config.handshake_pad_min + 16),
_mtu: config.mtu,
}) })
} }
pub fn in_flight_count(&self) -> usize { pub fn in_flight_count(&self) -> usize {
// COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not // COUNT ONLY retransmittable Data frames — control frames (Ack/Nack) must not
// contribute to this counter or they will trigger false backpressure. // contribute to this counter or they will trigger false backpressure.
self.sent_history.values().filter(|f| f.is_retransmittable).count() self.sent_history.iter().filter(|f| f.is_retransmittable).count()
} }
pub fn cwnd_packets(&self) -> usize { pub fn cwnd_packets(&self) -> usize {
self.cc.cwnd_packets() as usize self.cc.cwnd_packets() as usize
} }
/// Returns the current adaptive RTO (from congestion controller after first RTT sample,
/// falls back to the config-specified initial value before any ACK is received).
pub fn rto(&self) -> Duration {
self.cc.rto()
}
pub fn on_send(&mut self, bytes: u64) { pub fn on_send(&mut self, bytes: u64) {
self.cc.on_send(bytes); self.cc.on_send(bytes);
} }
@ -213,12 +207,13 @@ impl ProtocolMachine {
.map(ProtocolAction::SendDatagram) .map(ProtocolAction::SendDatagram)
} }
(OstpState::Closing, OstpEvent::Inbound(raw)) => { (OstpState::Closing, OstpEvent::Inbound(raw)) => {
// The remote may still have data or ACKs in transit. // Process final in-flight packets to prevent data loss during teardown.
// handle_inbound transitions to Closed when it receives a Close frame. // The remote may still have data or ACKs in transit when we initiated Close.
self.handle_inbound(raw) let result = self.handle_inbound(raw);
self.state = OstpState::Closed;
result
} }
(OstpState::Established, OstpEvent::Tick) => self.handle_tick(), (OstpState::Established, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closing, OstpEvent::Tick) => self.handle_tick(),
(OstpState::Closed, _) => Ok(ProtocolAction::Noop), (OstpState::Closed, _) => Ok(ProtocolAction::Noop),
(_, OstpEvent::Close) => { (_, OstpEvent::Close) => {
self.state = OstpState::Closed; self.state = OstpState::Closed;
@ -397,26 +392,18 @@ impl ProtocolMachine {
self.last_recv_advance = Instant::now(); self.last_recv_advance = Instant::now();
} else { } else {
// Gap detected // Gap detected
if self.reorder_buffer.len() >= self.max_reorder_buffer { if self.reorder_buffer.len() < self.max_reorder_buffer {
tracing::warn!("Reorder buffer full ({}/{}), dropping new frame nonce={} to wait for recovery of nonce={}", self.reorder_buffer.insert(nonce, action);
self.reorder_buffer.len(), self.max_reorder_buffer, nonce, self.expected_recv_nonce } else {
tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}",
self.reorder_buffer.len(), self.max_reorder_buffer, nonce
); );
} }
if nonce >= self.expected_recv_nonce { // Rate-limited NACK: send at most once per 30ms to prevent retransmit storms.
if self.reorder_buffer.len() < self.max_reorder_buffer { // Under high load with natural UDP reordering, sending a NACK per packet
self.reorder_buffer.insert(nonce, action); // causes exponential retransmit explosion that saturates the channel.
} else { let nack_cooldown = Duration::from_millis(30);
tracing::warn!("Reorder buffer still full after gap recovery, dropping frame nonce={}", nonce);
}
} else {
tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce);
}
// 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 { if self.last_nack_sent.elapsed() >= nack_cooldown {
self.last_nack_sent = Instant::now(); self.last_nack_sent = Instant::now();
let nack_payload = self.expected_recv_nonce.to_be_bytes(); let nack_payload = self.expected_recv_nonce.to_be_bytes();
@ -524,45 +511,68 @@ impl ProtocolMachine {
fn handle_tick(&mut self) -> Result<ProtocolAction, ProtocolError> { fn handle_tick(&mut self) -> Result<ProtocolAction, ProtocolError> {
let mut actions = Vec::new(); 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 ───────────────────────────────────────── // ── Pending ACK flush ─────────────────────────────────────────
if let Some(ack_frame) = self.build_ack_if_due()? { if let Some(ack_frame) = self.build_ack_if_due()? {
actions.push(ProtocolAction::SendDatagram(ack_frame)); actions.push(ProtocolAction::SendDatagram(ack_frame));
} }
let now = Instant::now(); let now = Instant::now();
// Use the adaptive RTO from the congestion controller (RFC 6298 SRTT + 4*RTTVAR). let base_rto_ms = self.rto.as_millis().max(1) as u64;
// Falls back to rto_initial before the first ACK is received.
let base_rto = self.cc.rto().max(self.rto_initial);
let base_rto_ms = base_rto.as_millis().max(1) as u64;
// ── Zombie frame eviction ──────────────────────────────────── // ── Zombie frame eviction ────────────────────────────────────
// Evict frames that exceeded max_retries + 2 grace retries. // 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 grace = self.max_retries.saturating_add(2);
let before = self.sent_history.len(); let before = self.sent_history.len();
self.sent_history.retain(|_, f| !f.is_retransmittable || f.retries <= grace); self.sent_history.retain(|f| !f.is_retransmittable || f.retries <= grace);
let evicted = before - self.sent_history.len(); let evicted = before - self.sent_history.len();
if evicted > 0 { if evicted > 0 {
tracing::debug!("Evicted {} zombie frames from sent_history (remaining={})", evicted, self.sent_history.len()); tracing::debug!("Evicted {} zombie frames from sent_history (remaining={})", evicted, self.sent_history.len());
} }
// ── Retransmit expired frames ──────────────────────────────── // ── Retransmit expired frames ────────────────────────────────
// Backoff starts from retry #0 (immediately effective): // Limit retransmits per tick to prevent bandwidth saturation
// effective_rto = base_rto * 2^retries, capped at 2^6 = 64×
// This ensures we do not flood with retransmits on the first few losses
// while still recovering quickly on a transient single loss.
let mut retransmit_budget: usize = self.cc.retransmit_budget(); let mut retransmit_budget: usize = self.cc.retransmit_budget();
for frame in self.sent_history.values_mut() { for frame in self.sent_history.iter_mut() {
if !frame.is_retransmittable { if !frame.is_retransmittable {
continue; continue;
} }
let backoff_factor = 1u64 << (frame.retries as u64).min(6); let retry_over = frame.retries.saturating_sub(self.max_retries);
let backoff_factor = 1u64 << retry_over.min(6);
let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor)); let effective_rto = Duration::from_millis(base_rto_ms.saturating_mul(backoff_factor));
if now.duration_since(frame.last_sent) >= effective_rto { if now.duration_since(frame.last_sent) >= effective_rto {
frame.last_sent = now; frame.last_sent = now;
frame.retries = frame.retries.saturating_add(1); frame.retries = frame.retries.saturating_add(1);
if retransmit_budget > 0 { if retransmit_budget > 0 {
actions.push(ProtocolAction::SendDatagram(frame.bytes.clone())); actions.push(ProtocolAction::SendDatagram(frame.bytes.clone()));
retransmit_budget -= 1; retransmit_budget -= 1;
@ -662,7 +672,7 @@ impl ProtocolMachine {
} }
fn lookup_sent_frame(&mut self, nonce: u64) -> Option<Bytes> { fn lookup_sent_frame(&mut self, nonce: u64) -> Option<Bytes> {
if let Some(frame) = self.sent_history.get_mut(&nonce) { if let Some(frame) = self.sent_history.iter_mut().rev().find(|f| f.nonce == nonce) {
frame.last_sent = Instant::now(); frame.last_sent = Instant::now();
frame.retries = frame.retries.saturating_add(1); frame.retries = frame.retries.saturating_add(1);
return Some(frame.bytes.clone()); return Some(frame.bytes.clone());
@ -674,7 +684,7 @@ impl ProtocolMachine {
if is_retransmittable { if is_retransmittable {
self.cc.on_send(bytes.len() as u64); self.cc.on_send(bytes.len() as u64);
} }
self.sent_history.insert(nonce, SentFrame { self.sent_history.push_back(SentFrame {
nonce, nonce,
bytes, bytes,
last_sent: Instant::now(), last_sent: Instant::now(),
@ -687,7 +697,7 @@ impl ProtocolMachine {
overflow, self.max_sent_history overflow, self.max_sent_history
); );
while self.sent_history.len() > self.max_sent_history { while self.sent_history.len() > self.max_sent_history {
self.sent_history.pop_first(); self.sent_history.pop_front();
} }
} }
} }
@ -698,8 +708,8 @@ impl ProtocolMachine {
let mut min_rtt = Duration::from_secs(60); let mut min_rtt = Duration::from_secs(60);
// Compute RTT from the oldest acked frame's send timestamp // Compute RTT from the oldest acked frame's send timestamp
for (&nonce, frame) in &self.sent_history { for frame in self.sent_history.iter() {
if nonce_in_ranges(nonce, ranges) { if nonce_in_ranges(frame.nonce, ranges) {
acked_bytes += frame.bytes.len() as u64; acked_bytes += frame.bytes.len() as u64;
let rtt = now.duration_since(frame.last_sent); let rtt = now.duration_since(frame.last_sent);
if rtt < min_rtt { if rtt < min_rtt {
@ -708,7 +718,7 @@ impl ProtocolMachine {
} }
} }
self.sent_history.retain(|&nonce, _| !nonce_in_ranges(nonce, ranges)); self.sent_history.retain(|frame| !nonce_in_ranges(frame.nonce, ranges));
// Notify congestion controller // Notify congestion controller
if acked_bytes > 0 { if acked_bytes > 0 {

View File

@ -95,15 +95,6 @@ class MainActivity : FlutterActivity() {
result.error("ERROR", e.message, null) result.error("ERROR", e.message, null)
} }
} }
"runDnsProber" -> {
try {
val domain = call.argument<String>("domain") ?: "example.com"
val json = net.ostp.client.OstpClientSdk.nativeRunDnsProber(domain)
result.success(json)
} catch (e: Throwable) {
result.error("ERROR", e.message, null)
}
}
"getInstalledApps" -> { "getInstalledApps" -> {
try { try {
val pm = packageManager val pm = packageManager

View File

@ -50,8 +50,4 @@ object OstpClientSdk {
@Keep @Keep
@JvmStatic @JvmStatic
external fun notifyNetworkChanged() external fun notifyNetworkChanged()
@Keep
@JvmStatic
external fun nativeRunDnsProber(domain: String): String
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -26,11 +26,11 @@ class OstpApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF030303), scaffoldBackgroundColor: const Color(0xFF08080F),
colorScheme: const ColorScheme.dark( colorScheme: const ColorScheme.dark(
primary: Color(0xFFF9FAFB), primary: Color(0xFF6C72FF),
secondary: Color(0xFF10B981), secondary: Color(0xFF22D3A5),
surface: Color(0xFF09090B), surface: Color(0xFF151522),
), ),
fontFamily: 'Inter', fontFamily: 'Inter',
useMaterial3: true, useMaterial3: true,

View File

@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../models/connection_state_enum.dart'; import '../models/connection_state_enum.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'logs_screen.dart'; import 'logs_screen.dart';
@ -518,16 +517,31 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
Center( Positioned(
child: Opacity( top: -150, right: -100,
opacity: theme.brightness == Brightness.dark ? 0.05 : 0.06, child: Container(
child: SvgPicture.asset( width: 400, height: 400,
'assets/logo.svg', decoration: BoxDecoration(
width: MediaQuery.of(context).size.width * 0.8, shape: BoxShape.circle,
fit: BoxFit.contain, color: theme.colorScheme.primary.withOpacity(0.15),
colorFilter: theme.brightness == Brightness.light ),
? const ColorFilter.mode(Colors.black, BlendMode.srcIn) child: BackdropFilter(
: null, filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
child: Container(),
),
),
),
Positioned(
bottom: -100, left: -100,
child: Container(
width: 350, height: 350,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.secondary.withOpacity(0.1),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
child: Container(),
), ),
), ),
), ),

View File

@ -32,13 +32,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
late TextEditingController _domainsCtrl; late TextEditingController _domainsCtrl;
late TextEditingController _ipsCtrl; late TextEditingController _ipsCtrl;
late TextEditingController _processesCtrl; late TextEditingController _processesCtrl;
late TextEditingController _dnsDomainCtrl; late TextEditingController _stealthSniCtrl;
late TextEditingController _pbkCtrl; late TextEditingController _pbkCtrl;
late TextEditingController _sidCtrl; late TextEditingController _sidCtrl;
bool _obscureKey = true; bool _obscureKey = true;
bool _debugMode = false; bool _debugMode = false;
late TextEditingController _dnsRegionCtrl; bool _wss = false;
String _transportMode = 'udp'; // 'udp' | 'uot' String _transportMode = 'udp'; // 'udp' | 'uot'
String _tunStack = 'ostp'; // 'system' | 'ostp' String _tunStack = 'ostp'; // 'system' | 'ostp'
bool _muxEnabled = false; bool _muxEnabled = false;
@ -57,10 +57,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
_domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? ''); _domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? ''); _ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? ''); _processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
_dnsDomainCtrl = TextEditingController(text: widget.prefs.getString('dns_domain') ?? ''); _stealthSniCtrl = TextEditingController(text: widget.prefs.getString('stealth_sni') ?? '');
_dnsRegionCtrl = TextEditingController(text: widget.prefs.getString('dns_region') ?? '1.1.1.1'); _pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? '');
_pbkCtrl = TextEditingController(text: widget.prefs.getString('tun_pbk') ?? '');
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? ''); _sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
_wss = widget.prefs.getBool('wss') ?? false;
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp'; _transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
_tunStack = widget.prefs.getString('tun_stack') ?? 'ostp'; _tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
_debugMode = widget.prefs.getBool('debug_mode') ?? false; _debugMode = widget.prefs.getBool('debug_mode') ?? false;
@ -80,8 +80,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_domainsCtrl.dispose(); _domainsCtrl.dispose();
_ipsCtrl.dispose(); _ipsCtrl.dispose();
_processesCtrl.dispose(); _processesCtrl.dispose();
_dnsDomainCtrl.dispose(); _stealthSniCtrl.dispose();
_dnsRegionCtrl.dispose();
_pbkCtrl.dispose(); _pbkCtrl.dispose();
_sidCtrl.dispose(); _sidCtrl.dispose();
_muxSessionsCtrl.dispose(); _muxSessionsCtrl.dispose();
@ -98,11 +97,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim()); widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
widget.prefs.setString('ex_processes', _processesCtrl.text.trim()); widget.prefs.setString('ex_processes', _processesCtrl.text.trim());
widget.prefs.setBool('debug_mode', _debugMode); widget.prefs.setBool('debug_mode', _debugMode);
widget.prefs.setBool('wss', _wss);
widget.prefs.setString('transport_mode', _transportMode); widget.prefs.setString('transport_mode', _transportMode);
widget.prefs.setString('tun_stack', _tunStack); widget.prefs.setString('tun_stack', _tunStack);
widget.prefs.setString('dns_domain', _dnsDomainCtrl.text.trim()); widget.prefs.setString('stealth_sni', _stealthSniCtrl.text.trim());
widget.prefs.setString('dns_region', _dnsRegionCtrl.text.trim()); widget.prefs.setString('pbk', _pbkCtrl.text.trim());
widget.prefs.setString('tun_pbk', _pbkCtrl.text.trim());
widget.prefs.setString('sid', _sidCtrl.text.trim()); widget.prefs.setString('sid', _sidCtrl.text.trim());
widget.prefs.setBool('mux_enabled', _muxEnabled); widget.prefs.setBool('mux_enabled', _muxEnabled);
widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim()); widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim());
@ -237,11 +236,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() { setState(() {
_serverCtrl.text = host; _serverCtrl.text = host;
_keyCtrl.text = key; _keyCtrl.text = key;
_dnsDomainCtrl.text = uri.queryParameters['domain'] ?? ''; _stealthSniCtrl.text = uri.queryParameters['sni'] ?? '';
_dnsRegionCtrl.text = uri.queryParameters['resolver'] ?? '1.1.1.1'; _pbkCtrl.text = uri.queryParameters['pbk'] ?? '';
_sidCtrl.text = uri.queryParameters['sid'] ?? '';
final type = uri.queryParameters['type']; _wss = uri.queryParameters['wss'] == 'true';
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : (type == 'dns' ? 'dns' : 'udp'); final type = uri.queryParameters['type'] ?? 'udp';
_transportMode = type == 'tcp' || type == 'http' ? 'uot' : 'udp';
_importCtrl.clear(); _importCtrl.clear();
_saveSettings(); _saveSettings();
@ -292,8 +292,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<String>( RadioListTile<String>(
value: 'udp', value: 'udp',
groupValue: _transportMode, groupValue: _transportMode,
title: const Text('UDP (Default)', style: TextStyle(fontWeight: FontWeight.w600)), title: const Text('UDP (по умолчанию)', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: const Text('Fast, works on Wi-Fi and most networks', style: TextStyle(color: Colors.white54, fontSize: 12)), subtitle: const Text('Быстро, работает через Wi-Fi и большинство сетей', style: TextStyle(color: Colors.white54, fontSize: 12)),
activeColor: Theme.of(context).colorScheme.secondary, activeColor: Theme.of(context).colorScheme.secondary,
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }), onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
), ),
@ -301,83 +301,111 @@ class _SettingsScreenState extends State<SettingsScreen> {
RadioListTile<String>( RadioListTile<String>(
value: 'uot', value: 'uot',
groupValue: _transportMode, groupValue: _transportMode,
title: const Text('UoT (UDP-over-TCP)', style: TextStyle(fontWeight: FontWeight.w600)), title: Wrap(
subtitle: const Text('Masks as HTTP stream, bypasses whitelists', style: TextStyle(color: Colors.white54, fontSize: 12)), crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
children: [
const Text('UoT (UDP-over-TCP)', style: TextStyle(fontWeight: FontWeight.w600)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF6C72FF).withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: const Text('xHTTP Стелс', style: TextStyle(fontSize: 10, color: Color(0xFF6C72FF), fontWeight: FontWeight.bold)),
),
],
),
subtitle: const Text('Маскировка под HTTP-поток, обходит белые списки (уровень 1)', style: TextStyle(color: Colors.white54, fontSize: 12)),
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }), onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
), ),
Divider(color: Colors.white.withOpacity(0.05), height: 1),
RadioListTile<String>(
value: 'dns',
groupValue: _transportMode,
title: const Text('DNS Proxy (Last Resort)', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: const Text('Very slow, but works under strict DPI blocks', style: TextStyle(color: Colors.orangeAccent, fontSize: 12)),
activeColor: Colors.orangeAccent,
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
),
], ],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildToggle('WebSocket (WSS)', 'Инкапсулировать транспорт в RFC 6455 (для строгого DPI)', _wss, (val) {
// DNS Proxy parameters setState(() {
_wss = val;
});
}),
const SizedBox(height: 16),
// Stealth parameters
AnimatedCrossFade( AnimatedCrossFade(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
crossFadeState: _transportMode == 'dns' ? CrossFadeState.showFirst : CrossFadeState.showSecond, crossFadeState: _transportMode == 'uot' ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: Container( firstChild: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.orangeAccent.withOpacity(0.06), color: const Color(0xFF6C72FF).withOpacity(0.06),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.orangeAccent.withOpacity(0.2)), border: Border.all(color: const Color(0xFF6C72FF).withOpacity(0.2)),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.dns, size: 16, color: Colors.orangeAccent), const Icon(Icons.security, size: 16, color: Color(0xFF6C72FF)),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('DNS Proxy Settings', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orangeAccent, fontSize: 14)), const Text('Стелс параметры', style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF6C72FF), fontSize: 14)),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
const Text( const Text(
'Specify the domain pointing to your server. Details in Wiki.', 'Укажи домен из белого списка. OSTP подключится к серверу и подделает SNI / HTTP Host.',
style: TextStyle(fontSize: 12, color: Colors.white38), style: TextStyle(fontSize: 12, color: Colors.white38),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField('Domain (Points to Server)', _dnsDomainCtrl, hint: 'tunnel.myvpn.com'), Builder(builder: (context) {
const SizedBox(height: 16), final List<String> domains = [
Row( 'yastatic.net', 'mc.yandex.ru', 'st.mycdn.me',
children: [ 'top-fwz1.mail.ru', 'sso.passport.yandex.ru',
Expanded( 'sberbank.ru', 'ad.mail.ru', 'ads.vk.com',
child: _buildTextField('DNS Resolver Server', _dnsRegionCtrl, hint: '1.1.1.1'), 'login.vk.com', 'api.sberbank.ru', 'ok.ru',
'rostelecom.ru', 'rt.ru', 'tinkoff.ru',
'x5.ru', 'ozon.ru', 'wildberries.ru', 'gosuslugi.ru', 'vk.com'
];
String currentVal = _stealthSniCtrl.text.trim();
if (currentVal.isEmpty) currentVal = 'vk.com';
if (!domains.contains(currentVal)) {
domains.add(currentVal);
}
return DropdownButtonFormField<String>(
value: currentVal,
dropdownColor: const Color(0xFF1E1E2C),
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
labelText: 'Стелс Домен (Автоподставление)',
labelStyle: const TextStyle(color: Colors.white54, fontSize: 13),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
), ),
const SizedBox(width: 8), items: domains.map((String domain) {
Padding( return DropdownMenuItem<String>(
padding: const EdgeInsets.only(top: 24.0), value: domain,
child: ElevatedButton( child: Text(domain),
onPressed: _showDnsProberDialog, );
style: ElevatedButton.styleFrom( }).toList(),
backgroundColor: Colors.orangeAccent.withOpacity(0.2), onChanged: (String? newValue) {
foregroundColor: Colors.orangeAccent, if (newValue != null) {
elevation: 0, setState(() {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), _stealthSniCtrl.text = newValue;
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), _saveSettings();
), });
child: const Text('PROBER', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), }
), },
) );
], }),
),
], ],
), ),
), ),
secondChild: const SizedBox.shrink(), secondChild: const SizedBox.shrink(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)), _buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)),
AnimatedCrossFade( AnimatedCrossFade(
@ -524,12 +552,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (host.isEmpty || key.isEmpty) return ''; if (host.isEmpty || key.isEmpty) return '';
final queryParams = <String>[]; final queryParams = <String>[];
if (_dnsDomainCtrl.text.trim().isNotEmpty) { if (_stealthSniCtrl.text.trim().isNotEmpty) {
queryParams.add('domain=${Uri.encodeComponent(_dnsDomainCtrl.text.trim())}'); queryParams.add('sni=${Uri.encodeComponent(_stealthSniCtrl.text.trim())}');
}
final resolver = _dnsRegionCtrl.text.trim();
if (resolver.isNotEmpty && resolver != '1.1.1.1') {
queryParams.add('resolver=${Uri.encodeComponent(resolver)}');
} }
if (_pbkCtrl.text.trim().isNotEmpty) { if (_pbkCtrl.text.trim().isNotEmpty) {
queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}'); queryParams.add('pbk=${Uri.encodeComponent(_pbkCtrl.text.trim())}');
@ -537,6 +561,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_sidCtrl.text.trim().isNotEmpty) { if (_sidCtrl.text.trim().isNotEmpty) {
queryParams.add('sid=${Uri.encodeComponent(_sidCtrl.text.trim())}'); queryParams.add('sid=${Uri.encodeComponent(_sidCtrl.text.trim())}');
} }
if (_wss) {
queryParams.add('wss=true');
}
if (_transportMode != 'udp') { if (_transportMode != 'udp') {
queryParams.add('type=$_transportMode'); queryParams.add('type=$_transportMode');
} }
@ -602,97 +629,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
Future<void> _showDnsProberDialog() async {
const channel = MethodChannel('com.ospab.ostp/vpn');
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return AlertDialog(
backgroundColor: Theme.of(context).colorScheme.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('DNS Prober', textAlign: TextAlign.center),
content: FutureBuilder<String?>(
future: channel.invokeMethod<String>('runDnsProber', {'domain': _dnsDomainCtrl.text.trim()}),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Sending real tunnel probes...', style: TextStyle(color: Colors.white54, fontSize: 13), textAlign: TextAlign.center),
],
);
}
if (snapshot.hasError || !snapshot.hasData) {
return Text('Error: ${snapshot.error}', style: const TextStyle(color: Colors.redAccent));
}
List<dynamic> results = [];
try {
results = jsonDecode(snapshot.data!);
} catch (_) {}
if (results.isEmpty) {
return const Text('No results or all timed out.', style: TextStyle(color: Colors.redAccent));
}
return SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: results.length,
itemBuilder: (context, index) {
final res = results[index];
final name = res['name'] ?? '';
final ip = res['ip'] ?? '';
final latency = res['latency_ms'];
final isBest = index == 0 && latency != null;
return ListTile(
onTap: latency != null ? () {
setState(() {
_dnsRegionCtrl.text = ip;
_saveSettings();
});
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('DNS set to $ip')));
} : null,
title: Text('${isBest ? '' : ''}$name', style: const TextStyle(fontSize: 14)),
subtitle: Text(ip, style: const TextStyle(fontSize: 12, color: Colors.white54)),
trailing: Text(
latency != null ? '$latency ms' : 'TIMEOUT',
style: TextStyle(
color: latency == null ? Colors.redAccent : (latency < 100 ? Colors.greenAccent : Colors.orangeAccent),
fontWeight: FontWeight.bold,
),
),
tileColor: isBest ? Colors.blueAccent.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
);
},
),
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
)
],
);
}
);
}
);
}
Future<void> _checkForUpdates() async { Future<void> _checkForUpdates() async {
if (_isCheckingUpdates) return; if (_isCheckingUpdates) return;
setState(() { _isCheckingUpdates = true; }); setState(() { _isCheckingUpdates = true; });

View File

@ -134,14 +134,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -280,14 +272,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -597,30 +581,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.5"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3"
url: "https://pub.dev"
source: hosted
version: "1.2.6"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.3.12+25 version: 0.3.4+17
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.4
@ -34,7 +34,6 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_svg: ^2.0.10
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5
mobile_scanner: ^5.0.0 mobile_scanner: ^5.0.0
window_manager: ^0.5.1 window_manager: ^0.5.1
@ -73,8 +72,9 @@ flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: # assets:
- assets/logo.svg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images

View File

@ -2665,7 +2665,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-client" name = "ostp-client"
version = "0.3.12" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
@ -2700,20 +2700,17 @@ dependencies = [
[[package]] [[package]]
name = "ostp-core" name = "ostp-core"
version = "0.3.12" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder",
"bytes", "bytes",
"chacha20poly1305", "chacha20poly1305",
"hkdf", "hkdf",
"hmac", "hmac",
"rand", "rand",
"serde",
"sha2", "sha2",
"snow", "snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio",
"tracing", "tracing",
"x25519-dalek", "x25519-dalek",
] ]
@ -2723,16 +2720,12 @@ name = "ostp-gui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chacha20poly1305",
"hex",
"json_comments", "json_comments",
"ostp-client", "ostp-client",
"ostp-core",
"portable-atomic", "portable-atomic",
"rand", "rand",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
@ -2742,7 +2735,7 @@ dependencies = [
[[package]] [[package]]
name = "ostp-tun" name = "ostp-tun"
version = "0.3.12" version = "0.2.98"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ostp-gui" name = "ostp-gui"
version = "0.1.0" version = "0.3.3"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@ -26,7 +26,6 @@ tokio = { version = "1", features = ["full"] }
anyhow = "1" anyhow = "1"
tracing = "0.1" tracing = "0.1"
ostp-client = { path = "../../ostp-client" } ostp-client = { path = "../../ostp-client" }
ostp-core = { path = "../../ostp-core" }
portable-atomic = "1" portable-atomic = "1"
json_comments = "0.2" json_comments = "0.2"
rand = "0.8" rand = "0.8"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Some files were not shown because too many files have changed in this diff Show More