Compare commits

..

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

119 changed files with 1427 additions and 4109 deletions

BIN
.gitignore vendored

Binary file not shown.

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.5"
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.5"
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.5"
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.5"
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.5"
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.5"
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.5"
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0" anyhow = "1.0"

212
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)
TUN[TUN Interface] -->|IP Packets| B
subgraph Client [Client Node] subgraph OSTPCoreClient ["OSTP Core Protocol"]
CoreC[OSTP Client] -.->|Encrypt & Mask| NetC[Transport Layer] 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 end
CoreS -->|Relay| WWW((Internet)) G -->|Active Probing / Unauth| FB[TCP Fallback Proxy]
Fallback -->|Forward| Web((Web / NGINX)) FB -->|Forward| NGINX[nginx / Caddy]
H -->|Stats & Traffic| API[Management API]
I -->|Outbound| WWW((Internet))
end
``` ```
--- ---
## 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)
TUN[TUN Интерфейс] -->|IP Пакеты| B
subgraph Client [Клиент] subgraph OSTPCoreClient ["OSTP Core Протокол"]
CoreC[OSTP Клиент] -.->|Шифрование| NetC[Транспортный уровень] 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 end
CoreS -->|Проксирование| WWW((Интернет)) G -->|Active Probing / Unauth| FB[TCP Fallback Proxy]
Fallback -->|Перенаправление| Web((Веб-сервер / NGINX)) FB -->|Перенаправление| NGINX[nginx / Caddy]
I -->|Outbound| WWW((Интернет))
end
``` ```
--- ---
## Быстрый старт ## Установка
### 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,17 +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с на сервере |
--- ---
## Сборка из исходников ## Сборка из исходников
```bash ```bash
# Требования: Rust 1.75+ # Требования: Rust toolchain (1.75+)
cargo build --release cargo build --release
# Кросс-компиляция для Linux # Кросс-компиляция для Linux
@ -154,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

@ -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.

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

@ -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,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,9 +10,9 @@ 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<()> {
println!("[ostp] Starting run_client_core with multi-server architecture"); println!("[ostp] Starting run_client_core with multi-server architecture");

View File

@ -1,230 +0,0 @@
/// DNS tunnel transport — dnstt-style implementation.
///
/// Protocol (client → server, embedded in DNS query domain name):
/// Base32([client_id: 8][msg_id: 2 BE][total_frags: 1][frag_idx: 1][payload: ≤MAX_CHUNK])
/// Split into DNS labels of max 63 chars, suffixed with base_domain.
///
/// Poll query: payload is empty (total_frags=1, frag_idx=0, len=0).
///
/// Protocol (server → client, in TXT rdata):
/// Concatenated length-prefixed OSTP packets: [len: 2 BE][data ...]...
///
/// Polling: adaptive 500ms → 10s, like dnstt. Resets to 500ms on real data.
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use rand::Rng;
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, Mutex};
use crate::transport::Transport;
use rand::RngCore;
use ostp_core::dns::{base32_encode, DnsPacket, DnsRecordType};
/// Max raw payload bytes we put into one DNS query.
/// Calculation: FQDN ≤ 253 chars. Domain suffix ~30 chars max.
/// Remaining: ~220 chars for base32 labels. 220/8*5 = 137 bytes raw.
/// Header = 12 bytes → payload ≤ 120 bytes (conservative, works for any domain ≤ 40 chars).
const MAX_CHUNK_PAYLOAD: usize = 120;
const CLIENT_ID_LEN: usize = 8;
const INIT_POLL_DELAY: Duration = Duration::from_millis(500);
const MAX_POLL_DELAY: Duration = Duration::from_secs(10);
const POLL_DELAY_MULTIPLIER: f64 = 2.0;
pub async fn start_dns_transport(
domain: String,
resolver: String,
_pubkey: Option<String>,
) -> std::io::Result<Transport> {
let (app_tx, transport_rx) = mpsc::channel::<Bytes>(256);
let (transport_tx, app_rx) = mpsc::channel::<Bytes>(256);
let resolver_addr = if resolver.contains(':') {
resolver.clone()
} else {
format!("{}:53", resolver)
};
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(&resolver_addr).await?;
let socket = Arc::new(socket);
// Generate random ClientID for this tunnel session
let mut client_id = [0u8; CLIENT_ID_LEN];
rand::thread_rng().fill_bytes(&mut client_id);
let client_id = Arc::new(client_id);
tracing::info!("DNS transport: domain={} resolver={} client_id={}",
domain, resolver_addr,
hex::encode(client_id.as_slice()));
// ── Send task ─────────────────────────────────────────────────────────────
let sock_send = socket.clone();
let cid_send = client_id.clone();
let domain_send = domain.clone();
tokio::spawn(async move {
let mut rx = transport_rx;
let mut msg_id: u16 = 0;
let mut poll_delay = INIT_POLL_DELAY;
loop {
let data: Option<Bytes> = tokio::select! {
data = rx.recv() => data,
_ = tokio::time::sleep(poll_delay) => {
poll_delay = Duration::from_secs_f64(
(poll_delay.as_secs_f64() * POLL_DELAY_MULTIPLIER)
.min(MAX_POLL_DELAY.as_secs_f64())
);
// Send poll (empty payload)
Some(Bytes::new())
}
};
let data = match data {
Some(d) => d,
None => {
tracing::debug!("DNS send task: channel closed, exiting");
break;
}
};
if data.is_empty() {
// Poll query — one empty chunk
if let Err(e) = send_chunk(&sock_send, &cid_send, msg_id, 1, 0, &[], &domain_send).await {
tracing::warn!("DNS poll send error: {}", e);
}
} else {
// Real OSTP packet — fragment into chunks
poll_delay = INIT_POLL_DELAY; // reset on real data
let data_slice = data.as_ref();
let total_chunks = data_slice.chunks(MAX_CHUNK_PAYLOAD).count();
let total_u8 = total_chunks.min(255) as u8;
for (idx, chunk) in data_slice.chunks(MAX_CHUNK_PAYLOAD).enumerate() {
if let Err(e) = send_chunk(
&sock_send, &cid_send,
msg_id, total_u8, idx as u8,
chunk, &domain_send,
).await {
tracing::warn!("DNS chunk send error (idx={}): {}", idx, e);
break;
}
// Brief inter-fragment delay to avoid flooding the resolver
if total_chunks > 1 {
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
msg_id = msg_id.wrapping_add(1);
}
}
});
// ── Receive task ──────────────────────────────────────────────────────────
let sock_recv = socket.clone();
let tx_recv = transport_tx.clone();
let domain_recv = domain.clone();
tokio::spawn(async move {
let mut buf = vec![0u8; 65535];
// Reassembly buffers: msg_id → (total, Vec<Option<chunk>>)
let reassembly: HashMap<u16, (u8, Vec<Option<Vec<u8>>>)> = HashMap::new();
loop {
match sock_recv.recv(&mut buf).await {
Ok(n) => {
let Some(pkt) = DnsPacket::decode(&buf[..n]) else { continue };
// Only process DNS responses
if pkt.flags & 0x8000 == 0 { continue; }
for answer in pkt.answers {
if answer.rtype != DnsRecordType::TXT && answer.rtype != DnsRecordType::NULL {
continue;
}
let rdata = answer.rdata;
// Parse length-prefixed OSTP packets packed in rdata:
// [len_hi: 1][len_lo: 1][data: len]...
let mut pos = 0;
while pos + 2 <= rdata.len() {
let pkt_len = u16::from_be_bytes([rdata[pos], rdata[pos + 1]]) as usize;
pos += 2;
if pkt_len == 0 { continue; }
if pos + pkt_len > rdata.len() {
tracing::debug!("DNS recv: truncated packet in rdata");
break;
}
let payload = Bytes::copy_from_slice(&rdata[pos..pos + pkt_len]);
pos += pkt_len;
if tx_recv.send(payload).await.is_err() {
return; // app closed
}
}
}
// Also check for responses packed in the server's extra DNS answer rdata
// that use our fragmentation scheme (server→client fragments)
// This is handled above via the length-prefix protocol.
let _ = &reassembly; // Keep for future upstream fragmentation support
let _ = &domain_recv;
}
Err(e) => {
tracing::warn!("DNS transport recv error: {}", e);
break;
}
}
}
});
Ok(Transport::Dns {
tx: app_tx,
rx: Arc::new(Mutex::new(app_rx)),
})
}
/// Build and send one DNS TXT query with a framed chunk.
///
/// Frame format (before base32 encoding):
/// [client_id: 8][msg_id: 2 BE][total_frags: 1][frag_idx: 1][payload: 0120]
async fn send_chunk(
socket: &UdpSocket,
client_id: &[u8; CLIENT_ID_LEN],
msg_id: u16,
total_frags: u8,
frag_idx: u8,
payload: &[u8],
base_domain: &str,
) -> std::io::Result<()> {
// Build frame
let mut frame = Vec::with_capacity(CLIENT_ID_LEN + 4 + payload.len());
frame.extend_from_slice(client_id);
frame.extend_from_slice(&msg_id.to_be_bytes());
frame.push(total_frags);
frame.push(frag_idx);
frame.extend_from_slice(payload);
// Base32-encode
let encoded = base32_encode(&frame);
// Split into 63-char labels and append domain
let mut fqdn = String::with_capacity(encoded.len() + base_domain.len() + 10);
let mut start = 0;
while start < encoded.len() {
let end = (start + 63).min(encoded.len());
fqdn.push_str(&encoded[start..end]);
fqdn.push('.');
start = end;
}
fqdn.push_str(base_domain);
// Build DNS TXT query with random ID
let id: u16 = rand::thread_rng().gen();
let pkt = DnsPacket::new_query(id, &fqdn, DnsRecordType::TXT);
let wire = pkt.encode();
tracing::trace!("DNS send chunk: msg_id={} frag={}/{} payload={}B fqdn_len={}",
msg_id, frag_idx + 1, total_frags, payload.len(), fqdn.len());
socket.send(&wire).await?;
Ok(())
}

View File

@ -1,4 +1,4 @@
pub mod dns;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use bytes::Bytes; use bytes::Bytes;
@ -9,10 +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>>>,
},
Dns {
tx: tokio::sync::mpsc::Sender<Bytes>,
rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Bytes>>>,
} }
} }
@ -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::Dns { 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,14 +26,14 @@ 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::Dns { .. } => 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::Dns { rx, .. } => { Self::Uot { rx, .. } => {
let mut rx = rx.lock().await; let mut rx = rx.lock().await;
match rx.recv().await { match rx.recv().await {
Some(bytes) => { Some(bytes) => {
@ -45,7 +41,7 @@ impl Transport {
buf[..len].copy_from_slice(&bytes[..len]); buf[..len].copy_from_slice(&bytes[..len]);
Ok(len) Ok(len)
} }
None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "channel closed")), None => Err(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "uot closed")),
} }
} }
} }
@ -54,7 +50,7 @@ impl Transport {
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::Dns { .. } => 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,185 +1,57 @@
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,
};
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: 5,
rto_ms: 100,
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 mut psk = [0u8; 32];
let key_bytes = access_key.as_bytes();
let len = key_bytes.len().min(32);
psk[..len].copy_from_slice(&key_bytes[..len]);
let config = ProtocolConfig {
role: ostp_core::NoiseRole::Initiator,
psk,
session_id: 1,
handshake_payload: vec![],
max_padding: 0,
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
obfuscation_key: [0; 8],
max_reorder: 16384,
max_reorder_buffer: 8192,
ack_delay_ms: 10,
rto_ms: 100,
max_retries: 5,
max_sent_history: 32768,
handshake_pad_min: 8,
handshake_pad_max: 24,
mtu: 1400,
};
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 mut machine = ProtocolMachine::new(config).unwrap();
let target_host_str = target_host.to_string();
let server_str = server.to_string();
// Spawn bridge task // Spawn bridge task
tokio::spawn(async move { tokio::spawn(async move {
// Send initial handshake
if let Ok(action) = machine.on_event(OstpEvent::Start) { if let Ok(action) = machine.on_event(OstpEvent::Start) {
handle_action(action, &transport, &mut server_stream).await; handle_action(action, &udp, &mut server_stream).await;
} }
// Wait for handshake response (server sends HandshakePayload back)
let mut buf = [0u8; 8192];
let mut handshake_success = false;
match tokio::time::timeout(
std::time::Duration::from_millis(15000),
transport.recv(&mut buf),
).await {
Ok(Ok(n)) => {
if let Ok(action) = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n]))) {
handle_action(action, &transport, &mut server_stream).await;
handshake_success = true;
}
}
_ => {
tracing::warn!("OSTP handshake timeout for {}:{}", server_str, port);
return;
}
}
if !handshake_success {
tracing::warn!("TCP handshake failed or protocol machine error");
return;
}
// 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 ───────────────────────
let mut buf = [0u8; 65535]; let mut buf = [0u8; 65535];
let mut udp_buf = [0u8; 65535]; let mut udp_buf = [0u8; 65535];
@ -187,27 +59,24 @@ pub async fn dial_tcp(
tokio::select! { tokio::select! {
Ok(n) = server_stream.read(&mut buf) => { Ok(n) = server_stream.read(&mut buf) => {
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(10)) => { _ = 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)
} }
@ -218,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,
} }
@ -294,38 +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> {
match transport_cfg.r#type.as_str() {
"dns" => {
let domain = transport_cfg.domain.clone()
.unwrap_or_else(|| "tunnel.example.com".to_string());
let resolver = transport_cfg.resolver.clone()
.unwrap_or_else(|| server.to_string());
let transport = crate::transport::dns::start_dns_transport(domain, resolver, transport_cfg.pubkey.clone()).await
.map_err(|e| anyhow::anyhow!(e))?;
Ok(transport)
}
_ => {
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;
} }
} }
} }
@ -333,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

@ -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,8 +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 use crypto::NoiseRole; pub use crypto::NoiseRole;
pub use framing::{TrafficProfile, PaddingStrategy}; pub use framing::{TrafficProfile, PaddingStrategy};

View File

@ -392,20 +392,12 @@ 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 {
tracing::warn!("Reorder buffer full ({}/{}), dropping new frame nonce={} to wait for recovery of nonce={}",
self.reorder_buffer.len(), self.max_reorder_buffer, nonce, self.expected_recv_nonce
);
}
if nonce >= self.expected_recv_nonce {
if self.reorder_buffer.len() < self.max_reorder_buffer { if self.reorder_buffer.len() < self.max_reorder_buffer {
self.reorder_buffer.insert(nonce, action); self.reorder_buffer.insert(nonce, action);
} else { } else {
tracing::warn!("Reorder buffer still full after gap recovery, dropping frame nonce={}", nonce); tracing::warn!("Reorder buffer full ({}/{}), dropping frame nonce={}",
} self.reorder_buffer.len(), self.max_reorder_buffer, nonce
} else { );
tracing::debug!("Frame nonce={} arrived too late after gap recovery, dropping", nonce);
} }
// Rate-limited NACK: send at most once per 30ms to prevent retransmit storms. // Rate-limited NACK: send at most once per 30ms to prevent retransmit storms.
@ -519,6 +511,32 @@ 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));

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,
activeColor: Theme.of(context).colorScheme.primary, spacing: 8,
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }), 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),
), ),
Divider(color: Colors.white.withOpacity(0.05), height: 1), child: const Text('xHTTP Стелс', style: TextStyle(fontSize: 10, color: Color(0xFF6C72FF), fontWeight: FontWeight.bold)),
RadioListTile<String>( ),
value: 'dns', ],
groupValue: _transportMode, ),
title: const Text('DNS Proxy (Last Resort)', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: const Text('Маскировка под HTTP-поток, обходит белые списки (уровень 1)', style: TextStyle(color: Colors.white54, fontSize: 12)),
subtitle: const Text('Very slow, but works under strict DPI blocks', style: TextStyle(color: Colors.orangeAccent, fontSize: 12)), activeColor: Theme.of(context).colorScheme.primary,
activeColor: Colors.orangeAccent,
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }), onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
), ),
], ],
), ),
), ),
const SizedBox(height: 16),
_buildToggle('WebSocket (WSS)', 'Инкапсулировать транспорт в RFC 6455 (для строгого DPI)', _wss, (val) {
setState(() {
_wss = val;
});
}),
const SizedBox(height: 16), const SizedBox(height: 16),
// DNS Proxy parameters // 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',
const SizedBox(width: 8), 'x5.ru', 'ozon.ru', 'wildberries.ru', 'gosuslugi.ru', 'vk.com'
Padding( ];
padding: const EdgeInsets.only(top: 24.0), String currentVal = _stealthSniCtrl.text.trim();
child: ElevatedButton( if (currentVal.isEmpty) currentVal = 'vk.com';
onPressed: _showDnsProberDialog, if (!domains.contains(currentVal)) {
style: ElevatedButton.styleFrom( domains.add(currentVal);
backgroundColor: Colors.orangeAccent.withOpacity(0.2), }
foregroundColor: Colors.orangeAccent, return DropdownButtonFormField<String>(
elevation: 0, value: currentVal,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), dropdownColor: const Color(0xFF1E1E2C),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), style: const TextStyle(color: Colors.white, fontSize: 14),
), decoration: InputDecoration(
child: const Text('PROBER', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), labelText: 'Стелс Домен (Автоподставление)',
), labelStyle: const TextStyle(color: Colors.white54, fontSize: 13),
) border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
], contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
), ),
items: domains.map((String domain) {
return DropdownMenuItem<String>(
value: domain,
child: Text(domain),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_stealthSniCtrl.text = newValue;
_saveSettings();
});
}
},
);
}),
], ],
), ),
), ),
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -6,15 +6,11 @@ description = "Enables access to core OSTP commands"
allow = [ allow = [
"start_tunnel", "start_tunnel",
"stop_tunnel", "stop_tunnel",
"reload_tunnel",
"get_tunnel_status", "get_tunnel_status",
"get_metrics", "get_metrics",
"get_config", "get_config",
"save_config", "save_config",
"get_wintun_install_path", "get_wintun_install_path",
"set_autostart", "set_autostart",
"get_autostart", "get_autostart"
"list_running_processes",
"kill_auto_search",
"run_dns_prober"
] ]

View File

@ -1,6 +0,0 @@
use ostp_core::dns_prober::{run_dns_prober as core_run_dns_prober, DnsProbeResult};
#[tauri::command]
pub async fn run_dns_prober(domain: String) -> Result<Vec<DnsProbeResult>, String> {
core_run_dns_prober(&domain).await
}

View File

@ -8,7 +8,6 @@ use portable_atomic::Ordering;
use tauri::Emitter; use tauri::Emitter;
mod ipc_crypto; mod ipc_crypto;
mod dns_prober;
// ── Config types ───────────────────────────────────────────────────────────── // ── Config types ─────────────────────────────────────────────────────────────
@ -779,7 +778,7 @@ static SINGLE_INSTANCE_LOCK: std::sync::OnceLock<std::net::TcpListener> = std::s
pub fn run() { pub fn run() {
if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") { if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") {
let _ = SINGLE_INSTANCE_LOCK.set(listener); let _ = SINGLE_INSTANCE_LOCK.set(listener);
} else if !cfg!(debug_assertions) { } else {
show_error_dialog("Приложение OSTP GUI уже запущено!"); show_error_dialog("Приложение OSTP GUI уже запущено!");
return; return;
} }
@ -875,7 +874,7 @@ pub fn run() {
} }
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes, dns_prober::run_dns_prober]) .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ostp-gui", "productName": "ostp-gui",
"version": "0.3.12", "version": "0.3.4",
"identifier": "com.ospab.ostp", "identifier": "com.ospab.ostp",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -30,18 +30,7 @@ const translations = {
label_transport: 'Transport Protocol', label_transport: 'Transport Protocol',
label_mtu: 'MTU Size', label_mtu: 'MTU Size',
label_transport: 'Transport Protocol', label_transport: 'Transport Protocol',
opt_udp: 'UDP (Default)', label_sni: 'Stealth SNI (Fake Host)',
opt_uot: 'TCP (UoT)',
opt_dns: 'DNS Proxy (Last Resort)',
label_dns_domain: 'Domain (Points to Server)',
dns_domain_hint: 'This is the "last resort" over public DNS servers. You need a domain pointing to your server (NS/A record).',
dns_guide: 'Detailed setup guide available in',
wiki_link: 'GitHub Wiki',
label_dns_region: 'DNS Resolver Region (Prober)',
opt_global: 'Global (Cloudflare, Google, etc)',
opt_russia: 'Russia (Yandex, VK, etc)',
opt_china: 'China (AliDNS, DNSPod, etc)',
opt_iran: 'Iran (Shatel, Electro, etc)',
label_mtu: 'MTU Size', label_mtu: 'MTU Size',
label_mux: 'Multiplexing (Mux)', label_mux: 'Multiplexing (Mux)',
@ -101,18 +90,7 @@ const translations = {
label_transport: 'Транспортный протокол', label_transport: 'Транспортный протокол',
label_mtu: 'Размер MTU', label_mtu: 'Размер MTU',
label_transport: 'Транспортный протокол', label_transport: 'Транспортный протокол',
opt_udp: 'UDP (по умолчанию)', label_sni: 'Маскировочный SNI',
opt_uot: 'TCP (UoT)',
opt_dns: 'DNS Proxy (Последний рубеж)',
label_dns_domain: 'Домен (указывает на сервер)',
dns_domain_hint: 'Это "последний рубеж" через публичные DNS сервера. Для работы нужен домен, указывающий на ваш сервер (NS/A запись).',
dns_guide: 'Подробный гайд по настройке доступен в',
wiki_link: 'Wiki на GitHub',
label_dns_region: 'Регион DNS Резолверов (Prober)',
opt_global: 'Global (Cloudflare, Google и др.)',
opt_russia: 'Россия (Yandex, VK и др.)',
opt_china: 'Китай (AliDNS, DNSPod и др.)',
opt_iran: 'Иран (Shatel, Electro и др.)',
label_mtu: 'Размер MTU', label_mtu: 'Размер MTU',
label_mux: 'Мультиплексирование (Mux)', label_mux: 'Мультиплексирование (Mux)',

View File

@ -12,9 +12,10 @@
<body> <body>
<div class="app-root"> <div class="app-root">
<!-- Eagle Watermark --> <!-- Ambient light blobs -->
<div class="watermark" aria-hidden="true"> <div class="ambient" aria-hidden="true">
<img src="assets/logo.svg" alt="" /> <div class="blob blob-1"></div>
<div class="blob blob-2"></div>
</div> </div>
<!-- ── HOME SCREEN ──────────────────────────────────────────── --> <!-- ── HOME SCREEN ──────────────────────────────────────────── -->
@ -195,27 +196,27 @@
<div class="field-group"> <div class="field-group">
<label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label> <label class="field-label" for="in-transport" data-i18n="label_transport">Transport Protocol</label>
<select id="in-transport" class="field-input"> <select id="in-transport" class="field-input">
<option value="udp" data-i18n="opt_udp">UDP (Default)</option> <option value="udp">UDP (Default)</option>
<option value="uot" data-i18n="opt_uot">TCP (UoT)</option> <option value="uot">TCP (UoT)</option>
<option value="dns" data-i18n="opt_dns">DNS Proxy (Last Resort)</option>
</select> </select>
</div> </div>
<div id="group-dns-proxy" style="display: none; flex-direction: column; gap: 14px;">
<div class="field-group"> <div class="field-group">
<label class="field-label" for="in-dns-domain" style="color: var(--c-warning);" data-i18n="label_dns_domain">Domain (Points to Server)</label> <label class="field-label" for="in-stealth-sni" data-i18n="label_sni">Stealth SNI</label>
<input id="in-dns-domain" class="field-input" type="text" placeholder="tunnel.myvpn.com" spellcheck="false" /> <input id="in-stealth-sni" class="field-input" type="text" placeholder="www.microsoft.com" spellcheck="false" />
</div> </div>
<div class="field-group"> <div class="toggle-row" id="group-wss">
<label class="field-label" for="in-dns-region" data-i18n="label_dns_region">DNS Resolver Server</label> <div class="toggle-text">
<div class="input-wrap"> <span class="toggle-name">WebSocket (WSS)</span>
<input id="in-dns-region" class="field-input" type="text" placeholder="1.1.1.1" spellcheck="false" /> <span class="toggle-hint">Use RFC 6455 framing for strict DPI bypass</span>
<button id="btn-dns-prober" class="peek-btn" tabindex="-1" title="Find fastest DNS server" style="width:auto; padding: 0 8px; font-size: 0.7rem; color: var(--c-accent);">
PROBER
</button>
</div>
</div> </div>
<label class="toggle">
<input type="checkbox" id="in-wss" />
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
</label>
</div> </div>
<div class="field-group"> <div class="field-group">
@ -375,23 +376,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- DNS Prober Modal -->
<div id="dns-prober-modal" class="modal-overlay hidden">
<div class="modal-content" style="min-width: 300px; max-width: 420px;">
<h3 class="modal-title">DNS Prober</h3>
<p class="modal-text" style="font-size: 0.8rem; color: var(--c-txt-2); margin-bottom: 12px;">
Sends a real DNS tunnel probe through each resolver and measures the round-trip time to your server. The fastest one is selected automatically.
</p>
<div id="prober-status" style="font-size: 0.75rem; color: var(--c-accent); margin-bottom: 10px; min-height: 18px;"></div>
<div id="prober-list" style="display: flex; flex-direction: column; gap: 5px; max-height: 280px; overflow-y: auto;"></div>
<div class="modal-actions" style="margin-top: 16px;">
<button id="btn-prober-close" class="btn secondary">Close</button>
</div>
</div>
</div>
</div> </div>
<script type="module" src="main.js"></script> <script type="module" src="main.js"></script>

View File

@ -45,9 +45,8 @@ const inDns = $('in-dns');
const groupCustomDns = $('group-custom-dns'); const groupCustomDns = $('group-custom-dns');
const inTransport = $('in-transport'); const inTransport = $('in-transport');
const groupDnsProxy = $('group-dns-proxy'); const inSni = $('in-stealth-sni');
const inDnsDomain = $('in-dns-domain'); const inWss = $('in-wss');
const inDnsRegion = $('in-dns-region');
const inMtu = $('in-mtu'); const inMtu = $('in-mtu');
const inTun = $('in-tun-mode'); const inTun = $('in-tun-mode');
const inKillSwitch = $('in-kill-switch'); const inKillSwitch = $('in-kill-switch');
@ -57,117 +56,11 @@ const inDebug = $('in-debug');
const inAutoconnect = $('in-autoconnect'); const inAutoconnect = $('in-autoconnect');
const inLaunchStartup = $('in-launch-startup'); const inLaunchStartup = $('in-launch-startup');
function bindSettingsInputs() {
const ids = [
'in-server', 'in-key', 'in-socks', 'in-dns',
'in-transport', 'in-dns-domain', 'in-dns-region',
'in-mtu', 'in-mux-sessions',
'in-tun-mode', 'in-kill-switch', 'in-mux-mode',
'in-debug', 'in-autoconnect', 'in-launch-startup'
];
ids.forEach(id => {
const el = $(id);
if (el) el.addEventListener('change', scheduleAutoSave);
if (el && el.type === 'text') el.addEventListener('input', scheduleAutoSave);
if (el && el.type === 'password') el.addEventListener('input', scheduleAutoSave);
});
if (inTransport) {
inTransport.addEventListener('change', () => {
if (inTransport.value === 'dns') {
groupDnsProxy.style.display = 'flex';
} else {
groupDnsProxy.style.display = 'none';
}
});
}
}
const wintunModal = $('wintun-modal'); const wintunModal = $('wintun-modal');
const btnWintunCancel = $('btn-wintun-cancel'); const btnWintunCancel = $('btn-wintun-cancel');
const btnWintunOpen = $('btn-wintun-open'); const btnWintunOpen = $('btn-wintun-open');
const wintunInstallPath = $('wintun-install-path'); const wintunInstallPath = $('wintun-install-path');
const dnsProberModal = $('dns-prober-modal');
const proberStatus = $('prober-status');
const proberList = $('prober-list');
const btnProberClose = $('btn-prober-close');
const btnDnsProber = $('btn-dns-prober');
// ── DNS Prober ───────────────────────────────────────────────────────────────
async function openDnsProber() {
dnsProberModal.classList.remove('hidden');
proberList.innerHTML = '';
proberStatus.textContent = 'Running probes...';
const domain = inDnsDomain?.value?.trim() || 'example.com';
let results;
try {
results = await invoke('run_dns_prober', { domain });
} catch (err) {
proberStatus.textContent = 'Error: ' + err;
return;
}
proberList.innerHTML = '';
if (!results || results.length === 0) {
proberStatus.textContent = 'No results.';
return;
}
let bestIp = null;
results.forEach((r, i) => {
const isBest = i === 0 && r.latency_ms != null;
if (isBest && !bestIp) bestIp = r.ip;
const row = document.createElement('div');
row.style.cssText = `
display: flex; align-items: center; justify-content: space-between;
padding: 6px 10px; border-radius: 6px; cursor: pointer;
background: ${isBest ? 'rgba(99,179,237,0.12)' : 'rgba(255,255,255,0.04)'};
border: 1px solid ${isBest ? 'rgba(99,179,237,0.35)' : 'transparent'};
transition: background 0.15s;
`;
const latText = r.latency_ms != null ? `${r.latency_ms} ms` : 'TIMEOUT';
const latColor = r.latency_ms == null ? '#f56565'
: r.latency_ms < 50 ? '#68d391'
: r.latency_ms < 150 ? '#f6e05e'
: '#fc8181';
row.innerHTML = `
<span style="font-size:0.78rem; color: var(--c-txt-1);">${isBest ? '⭐ ' : ''}${r.name}</span>
<span style="font-size:0.78rem; color: var(--c-txt-2);">${r.ip}</span>
<span style="font-size:0.78rem; font-weight:600; color:${latColor};">${latText}</span>
`;
if (r.latency_ms != null) {
row.addEventListener('click', () => {
inDnsRegion.value = r.ip;
scheduleAutoSave();
dnsProberModal.classList.add('hidden');
showToast('DNS server set to ' + r.ip, 'ok');
});
row.addEventListener('mouseenter', () => { row.style.background = 'rgba(99,179,237,0.18)'; });
row.addEventListener('mouseleave', () => { row.style.background = isBest ? 'rgba(99,179,237,0.12)' : 'rgba(255,255,255,0.04)'; });
}
proberList.appendChild(row);
});
if (bestIp) {
proberStatus.textContent = `✓ Best: ${bestIp} — click any row to select`;
// Auto-fill best
inDnsRegion.value = bestIp;
scheduleAutoSave();
} else {
proberStatus.textContent = 'All servers timed out.';
}
}
// ── Tag-input state ─────────────────────────────────────────────────────────── // ── Tag-input state ───────────────────────────────────────────────────────────
// Map of tagId -> Set<string> // Map of tagId -> Set<string>
const tagState = { const tagState = {
@ -445,13 +338,8 @@ async function loadConfigIntoForm() {
inServer.value = ostpOut.server ? `${ostpOut.server}:${ostpOut.port || 50000}` : ''; inServer.value = ostpOut.server ? `${ostpOut.server}:${ostpOut.port || 50000}` : '';
inKey.value = ostpOut.access_key || ''; inKey.value = ostpOut.access_key || '';
inTransport.value = ostpOut.transport?.type || 'udp'; inTransport.value = ostpOut.transport?.type || 'udp';
if (inTransport.value === 'dns') { inSni.value = ostpOut.transport?.stealth_sni || '';
groupDnsProxy.style.display = 'flex'; inWss.checked = !!ostpOut.transport?.wss;
inDnsDomain.value = ostpOut.transport?.domain || '';
inDnsRegion.value = ostpOut.transport?.resolver || 'Global';
} else {
groupDnsProxy.style.display = 'none';
}
inMux.checked = !!ostpOut.multiplex?.enabled; inMux.checked = !!ostpOut.multiplex?.enabled;
inMuxSessions.value = ostpOut.multiplex?.sessions || ''; inMuxSessions.value = ostpOut.multiplex?.sessions || '';
} }
@ -494,11 +382,8 @@ async function loadConfigIntoForm() {
inKey.value = c.access_key || ''; inKey.value = c.access_key || '';
inSocks.value = c.socks5_bind || '127.0.0.1:1088'; inSocks.value = c.socks5_bind || '127.0.0.1:1088';
inTransport.value = c.transport?.mode || 'udp'; inTransport.value = c.transport?.mode || 'udp';
if (inTransport.value === 'dns') { inSni.value = c.transport?.stealth_sni || '';
groupDnsProxy.style.display = 'block'; inWss.checked = !!c.transport?.wss;
} else {
groupDnsProxy.style.display = 'none';
}
inMtu.value = c.mtu || ''; inMtu.value = c.mtu || '';
inTun.checked = !!c.tun?.enable; inTun.checked = !!c.tun?.enable;
@ -581,8 +466,8 @@ async function handleSave(silent = false) {
access_key: key, access_key: key,
transport: { transport: {
type: inTransport.value, type: inTransport.value,
domain: inTransport.value === 'dns' ? inDnsDomain.value.trim() : undefined, stealth_sni: inSni.value.trim() || undefined,
resolver: inTransport.value === 'dns' ? inDnsRegion.value : undefined wss: inWss.checked ? true : undefined
}, },
multiplex: inMux.checked ? { multiplex: inMux.checked ? {
enabled: true, enabled: true,
@ -642,8 +527,7 @@ function handleImport() {
if (!key || !host) throw new Error('Incomplete link parameters'); if (!key || !host) throw new Error('Incomplete link parameters');
inServer.value = host; inServer.value = host;
inKey.value = key; inKey.value = key;
inTransport.value = 'udp'; inSni.value = url.searchParams.get('sni') || '';
groupDnsProxy.style.display = 'none';
const type = url.searchParams.get('type'); const type = url.searchParams.get('type');
if (type === 'tcp' || type === 'http') inTransport.value = 'uot'; if (type === 'tcp' || type === 'http') inTransport.value = 'uot';
@ -672,7 +556,6 @@ window.addEventListener('DOMContentLoaded', async () => {
applyTranslations(); applyTranslations();
setState('disconnected'); setState('disconnected');
updateKillSwitchVisibility(); updateKillSwitchVisibility();
bindSettingsInputs();
// Event wiring // Event wiring
if (window.__TAURI__ && window.__TAURI__.event) { if (window.__TAURI__ && window.__TAURI__.event) {
@ -818,22 +701,6 @@ window.addEventListener('DOMContentLoaded', async () => {
wintunModal.classList.add('hidden'); wintunModal.classList.add('hidden');
}); });
// DNS Prober modal
if (btnDnsProber) {
btnDnsProber.addEventListener('click', openDnsProber);
}
if (btnProberClose) {
btnProberClose.addEventListener('click', () => {
dnsProberModal.classList.add('hidden');
});
}
// Close prober on backdrop click
if (dnsProberModal) {
dnsProberModal.addEventListener('click', (e) => {
if (e.target === dnsProberModal) dnsProberModal.classList.add('hidden');
});
}
// Open wintun.net link — handled natively by <a target="_blank">, but also wire as fallback // Open wintun.net link — handled natively by <a target="_blank">, but also wire as fallback
if (btnWintunOpen && window.__TAURI__) { if (btnWintunOpen && window.__TAURI__) {
btnWintunOpen.addEventListener('click', (e) => { btnWintunOpen.addEventListener('click', (e) => {

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