feat: make panel open source, remove license check, and restore rust-embed

This commit is contained in:
ospab 2026-06-18 22:54:31 +03:00
parent 9e2ab59121
commit 5782107c84
27 changed files with 1688 additions and 547 deletions

5
.gitignore vendored
View File

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

35
Cargo.lock generated
View File

@ -1540,6 +1540,7 @@ 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",
@ -1917,6 +1918,40 @@ 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"

214
README.md
View File

@ -1,131 +1,101 @@
# 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](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](docs/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: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=for-the-badge) ![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.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)
> A fast, custom encrypted transport protocol written in Rust. 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.
**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](MIGRATION_V0_3_1.md). > **Upgrading from v0.2.x?** Please read the [v0.3.1 Configuration Migration Guide](docs/migration_v0_3_1.md).
--- ---
## Quick Install ## Technical Capabilities
### Linux | Capability | Description |
```bash |------------|-------------|
bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh) | **Traffic Masking** | Header and payload encryption using per-packet HMAC-derived keys. Indistinguishable from random noise. |
``` | **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. |
| **Seamless Roaming** | Clients can switch networks (WiFi ↔ LTE) without session interruption — tracked by session-ID, not IP. | | **Session Roaming** | Connection persistence across IP changes via session ID tracking. |
| **Management API** | Built-in REST API for third-party panels (3x-ui, custom dashboards). Per-user stats, traffic limits, key CRUD. | | **UoT Mode** | UDP-over-TCP encapsulation with length-prefixing to bypass UDP blocking. |
| **Fallback Server** | TCP fallback proxy to a web server — makes OSTP indistinguishable from nginx during active probing. | | **Fallback Server** | TCP proxying to a legitimate web server to resist active probing. |
| **Multi-Listener** | Bind to multiple addresses simultaneously (dual-stack IPv4/IPv6, multi-port). | | **TUN Mode** | Native network stack integration (`smoltcp`) for full-system routing without external dependencies. |
| **TUN Mode** | Full-system VPN via native `smoltcp` network stack without external dependencies. All traffic transparently routed through the tunnel. | | **Management API** | Built-in REST API for server administration, metrics, and key generation. |
| **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. | | **TURN Relay** | RFC 5766 TURN support for NAT traversal. |
| **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
graph TD flowchart LR
subgraph Client ["Client"] Apps[Local Apps] -->|SOCKS5 / TUN| CoreC
A[Browser / Apps] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
TUN[TUN Interface] -->|IP Packets| B
subgraph OSTPCoreClient ["OSTP Core Protocol"] subgraph Client [Client Node]
B --> C{Protocol Machine} CoreC[OSTP Client] -.->|Encrypt & Mask| NetC[Transport Layer]
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Obfuscated UDP Payload| E((UDP Socket))
end
end end
E <==>|Encrypted & Obfuscated UDP Tunnel| F NetC <==>|Encrypted UDP / UoT| NetS
subgraph Server ["Server"] subgraph Server [Server Node]
F((UDP Socket)) --> G{Dispatcher} NetS[Transport Layer] -.->|Decrypt & Auth| CoreS[OSTP Server]
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
G -->|Active Probing / Unauth| FB[TCP Fallback Proxy] CoreS -->|Relay| WWW((Internet))
FB -->|Forward| NGINX[nginx / Caddy] Fallback -->|Forward| Web((Web / NGINX))
H -->|Stats & Traffic| API[Management API]
I -->|Outbound| WWW((Internet))
end
``` ```
--- ---
## Quick Start ## Quick Start
### 1. Generate config ### 1. Installation
**Linux:**
```bash ```bash
# On your VPS (server): bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)
```
**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 your machine (client): # On the client:
./ostp --init client ./ostp --init client
``` ```
### 2. Edit config **Server Example** (`config.json`):
**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** — point to your server: **Client Example** (`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": [
{ {
@ -135,90 +105,35 @@ graph TD
"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"
} }
]
} }
``` ```
> **Note:** Upgrading from v0.2.x? Read the [v0.3.1 Migration Guide](MIGRATION_V0_3_1.md). ### 3. Execution
### 3. Run
```bash ```bash
./ostp # Uses config.json in current directory # Run with default config.json
./ostp --config /path/to.json # Custom config path ./ostp
./ostp --check # Validate config without running
./ostp --generate-key # Generate a new access key # Run with a specific config path
./ostp --links # Print client share links ./ostp --config /path/to/config.json
``` ```
### 4. Connect via share link (one-liner) Or connect via a one-line share link on the client:
```bash ```bash
./ostp "ostp://ACCESS_KEY@server.com:50000?..." ./ostp "ostp://YOUR_SECRET_KEY@YOUR_SERVER_IP:50000?transport=udp"
```
> [!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 Summary ## Protocol Specification
| 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 Obfuscation | HMAC-SHA256 derived per-packet mask | | Header Masking | 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 |
@ -228,38 +143,31 @@ Arguments:
## Building from Source ## Building from Source
```bash ```bash
# Prerequisites: Rust 1.75+ # Requires 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)** — Full documentation - **[Wiki](https://github.com/ospab/ostp/wiki)**
- [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
Business Source License 1.1. Free for personal and non-commercial use. GNU Affero General Public License v3.0 (AGPL-3.0). See [LICENSE](LICENSE) for more details.
Converts to MIT License on May 14, 2030.
--- ---
## Contact ## Contacts
- **Telegram**: [@ospab0](https://t.me/ospab0) - **Telegram**: [@ospab0](https://t.me/ospab0)
- **Email**: gvoprgrg@gmail.com - **Email**: gvoprgrg@gmail.com

View File

@ -1,123 +1,101 @@
# 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](MIGRATION_V0_3_1.md) [English](README.md) · [Wiki](https://github.com/ospab/ostp/wiki) · [Contributing](CONTRIBUTING.ru.md) · [Миграция v0.3.1](docs/migration_v0_3_1_ru.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: BSL 1.1](https://img.shields.io/badge/License-BSL%201.1-orange.svg?style=for-the-badge) ![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.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)
> Быстрый кастомный зашифрованный транспортный протокол на Rust. OSTP (Ospab Stealth Transport Protocol) — зашифрованный транспортный протокол, написанный на Rust. Реализует механизм ARQ поверх UDP, а также режим UoT (UDP-over-TCP). Протокол использует криптографическое маскирование заголовков и полезной нагрузки для защиты от систем глубокого анализа трафика (DPI).
**OSTP** (Ospab Stealth Transport Protocol) — кастомный транспортный протокол. Реализует собственный ARQ-транспорт поверх UDP, а также режим UoT (UDP-over-TCP). Каждый байт, включая заголовки пакетов, криптографически неотличим от случайного шума, что делает его устойчивым к системам глубокого анализа трафика (DPI).
> [!IMPORTANT] > [!IMPORTANT]
> **Обновляетесь с версии v0.2.x?** Пожалуйста, ознакомьтесь с [Руководством по миграции конфигурации v0.3.1](MIGRATION_V0_3_1.md). > **Обновляетесь с версии v0.2.x?** Пожалуйста, ознакомьтесь с [Руководством по миграции конфигурации v0.3.1](docs/migration_v0_3_1_ru.md).
--- ---
## Возможности ## Технические характеристики
| Возможность | Описание | | Возможность | Описание |
|-------------|----------| |-------------|----------|
| **Обфускация трафика** | Каждый пакет, включая заголовки, неотличим от случайного шума. Session ID и nonce маскируются HMAC-ключами, уникальными для каждого пакета. | | **Маскирование трафика** | Шифрование заголовков и полезной нагрузки с помощью 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. Разработан для 10 Гбит/с. | | **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. |
| **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. | | **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. |
| **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. | | **Session Roaming** | Сохранение соединения при смене IP-адреса благодаря отслеживанию по идентификатору сессии (session ID). |
| **TUN-режим** | Полносистемный VPN без внешних зависимостей (встроенный network stack на базе `smoltcp`). | | **Режим UoT** | Инкапсуляция UDP внутри TCP с указанием длины пакетов для обхода блокировок неизвестного UDP-трафика. |
| **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, который полностью скрывает трафик. Поскольку все данные полностью зашифрованы и имеют префикс длины, он обходит DPI фильтры, блокирующие неизвестный UDP трафик, передавая всё по обычному TCP соединению. | | **Fallback Server** | Проксирование неаутентифицированных TCP подключений на веб-сервер для защиты от активного пробинга. |
| **Мобильные приложения** | Красивый кроссплатформенный мобильный клиент (Flutter) для удобного администрирования. | | **TUN-режим** | Полносистемная маршрутизация через встроенный сетевой стек `smoltcp` без внешних зависимостей. |
| **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. | | **Management API** | Встроенный REST API для администрирования сервера, сбора метрик и генерации ключей. |
| **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). | | **TURN Relay** | Поддержка RFC 5766 TURN для обхода NAT. |
| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. |
--- ---
## Архитектура ## Архитектура
```mermaid ```mermaid
graph TD flowchart LR
subgraph Client ["Клиент"] Apps[Приложения] -->|SOCKS5 / TUN| CoreC
A[Браузер / Прил.] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
TUN[TUN Интерфейс] -->|IP Пакеты| B
subgraph OSTPCoreClient ["OSTP Core Протокол"] subgraph Client [Клиент]
B --> C{Protocol Machine} CoreC[OSTP Клиент] -.->|Шифрование| NetC[Транспортный уровень]
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Обфусцированный UDP| E((UDP Сокет))
end
end end
E <==>|Зашифрованный UDP Туннель| F NetC <==>|Зашифрованный UDP / UoT| NetS
subgraph Server ["Сервер"] subgraph Server [Сервер]
F((UDP Сокет)) --> G{Dispatcher} NetS[Транспортный уровень] -.->|Дешифрование| CoreS[OSTP Сервер]
NetS -->|Неавторизованные| Fallback[Fallback Сервер]
subgraph OSTPCoreServer ["OSTP Core Backend"]
G -->|Auth & Decrypt| H[Session & State Guard]
H -->|TCP Поток| I[Relay Loop]
end end
G -->|Active Probing / Unauth| FB[TCP Fallback Proxy] CoreS -->|Проксирование| WWW((Интернет))
FB -->|Перенаправление| NGINX[nginx / Caddy] Fallback -->|Перенаправление| Web((Веб-сервер / NGINX))
I -->|Outbound| WWW((Интернет))
end
``` ```
--- ---
## Установка ## Быстрый старт
### Linux ### 1. Установка
**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 client # Локальная машина ./ostp --init server
# На клиенте:
./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": [
{ {
@ -126,41 +104,26 @@ 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"
} }
]
} }
``` ```
> **Примечание:** Обновляетесь с v0.2.x? Прочтите [Гайд по миграции на v0.3.1](MIGRATION_V0_3_1.md). ### 3. Запуск
---
## Использование
```bash ```bash
# Запуск с конфигом # Запуск с конфигурацией по умолчанию (config.json)
./ostp --config config.json
# Или просто (ищет config.json рядом с бинарником)
./ostp ./ostp
# Запуск с указанием пути к конфигурации
./ostp --config /path/to/config.json
``` ```
### TUN-режим (Windows) Либо подключение через однострочную ссылку на стороне клиента:
Использует встроенный сетевой стек `smoltcp` и виртуальный адаптер `wintun` (необходима `wintun.dll`). Требует запуска с правами Администратора. ```bash
./ostp "ostp://ВАШ_КЛЮЧ@IP_СЕРВЕРА:50000?transport=udp"
### TUN-режим (Linux) ```
Использует встроенный сетевой стек `smoltcp` и `/dev/net/tun`. Требует запуска от имени `root` (или наличия `CAP_NET_ADMIN`).
--- ---
@ -170,19 +133,17 @@ 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 (30мс cooldown) + exponential backoff RTO | | Ретрансмиссия | Rate-limited NACK + exponential backoff RTO |
| Flow Control | Окно in-flight (только retransmittable фреймы) |
| Keepalive | Ping/Pong с измерением RTT каждые 5с | | Keepalive | Ping/Pong с измерением RTT каждые 5с |
| Таймаут сессии | 60с на клиенте, 300с на сервере |
--- ---
## Сборка из исходников ## Сборка из исходников
```bash ```bash
# Требования: Rust toolchain (1.75+) # Требования: Rust 1.75+
cargo build --release cargo build --release
# Кросс-компиляция для Linux # Кросс-компиляция для Linux
@ -193,16 +154,21 @@ cross build --release --target x86_64-unknown-linux-gnu
## Документация ## Документация
- [Архитектура](docs/ru/architecture.md) - **[Wiki](https://github.com/ospab/ostp/wiki)**
- [Спецификация протокола](docs/ru/specification.md) - [Спецификация протокола](docs/ru/specification.md)
- [Дизайн обфускации](docs/ru/obfuscation.md) - [Дизайн обфускации](docs/ru/obfuscation.md)
- [Администрирование сервера](docs/ru/server.md) - [Архитектура](docs/ru/architecture.md)
- [Настройка клиента](docs/ru/client.md) - [Настройка клиента](docs/ru/client.md)
- [Интеграции](docs/ru/integrations.md)
--- ---
## Лицензия ## Лицензия
Business Source License 1.1. Бесплатно для личного и некоммерческого использования. GNU Affero General Public License v3.0 (AGPL-3.0). Подробнее см. в файле [LICENSE](LICENSE).
Переходит в MIT License 14 мая 2030 года.
---
## Контакты
- **Telegram**: [@ospab0](https://t.me/ospab0)
- **Email**: gvoprgrg@gmail.com

102
docs/migration_v0_3_1.md Normal file
View File

@ -0,0 +1,102 @@
# 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.

36
fix_delimiter.py Normal file
View File

@ -0,0 +1,36 @@
import os
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
old_block = ''' if let Some(key) = first_key {
let host = get_or_ask_public_ip(&args.config);
let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:{}", key, host, port);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
}
println!(" [1] {}", link);
}'''
new_block = ''' if let Some(key) = first_key {
let host = get_or_ask_public_ip(&args.config);
let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:{}", key, host, port);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
}
println!(" [1] {}", link);
}
}'''
content = content.replace(old_block, new_block)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

View File

@ -15,8 +15,6 @@ 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)]
@ -305,10 +303,6 @@ 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)
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
ostp-control/dist/favicon.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
ostp-control/dist/icons.svg vendored Normal file
View File

@ -0,0 +1,24 @@
<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>

After

Width:  |  Height:  |  Size: 4.9 KiB

14
ostp-control/dist/index.html vendored Normal file
View File

@ -0,0 +1,14 @@
<!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

@ -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.4+17 version: 0.3.6+19
environment: environment:
sdk: ^3.11.4 sdk: ^3.11.4

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ostp-gui" name = "ostp-gui"
version = "0.3.3" version = "0.1.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"

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.4", "version": "0.3.6",
"identifier": "com.ospab.ostp", "identifier": "com.ospab.ostp",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"

View File

@ -22,6 +22,7 @@ hmac.workspace = true
sha2.workspace = true sha2.workspace = true
base64 = "0.22" base64 = "0.22"
mime_guess = "2.0" mime_guess = "2.0"
rust-embed = "8.4"
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
futures-util = "0.3" futures-util = "0.3"

View File

@ -53,7 +53,6 @@ pub struct ApiState {
pub dns_server: std::sync::Arc<crate::dns::DnsServer>, pub dns_server: std::sync::Arc<crate::dns::DnsServer>,
pub audit_logs: Arc<RwLock<Vec<AuditLogEntry>>>, pub audit_logs: Arc<RwLock<Vec<AuditLogEntry>>>,
pub router: std::sync::Arc<crate::router::Router>, pub router: std::sync::Arc<crate::router::Router>,
pub is_licensed: bool,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -172,8 +171,33 @@ fn api_unauthorized<T: Serialize>() -> (StatusCode, Json<ApiResponse<T>>) {
(StatusCode::UNAUTHORIZED, Json(ApiResponse { ok: false, data: None, error: Some("unauthorized".to_string()) })) (StatusCode::UNAUTHORIZED, Json(ApiResponse { ok: false, data: None, error: Some("unauthorized".to_string()) }))
} }
async fn static_handler(State(_state): State<ApiState>, _uri: Uri) -> impl IntoResponse { #[derive(rust_embed::RustEmbed)]
(StatusCode::NOT_FOUND, "Control panel not bundled").into_response() #[folder = "../ostp-control/dist"]
struct Assets;
async fn static_handler(State(state): State<ApiState>, uri: Uri) -> impl IntoResponse {
let mut path = uri.path().trim_start_matches(&format!("/{}", state.webpath.trim_matches('/'))).to_string();
if path.starts_with('/') {
path.remove(0);
}
let path = if path.is_empty() { "index.html".to_string() } else { path };
match Assets::get(&path) {
Some(content) => {
let mime = mime_guess::from_path(&path).first_or_octet_stream();
([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => {
if path.contains('.') {
(StatusCode::NOT_FOUND, "404 Not Found").into_response()
} else {
match Assets::get("index.html") {
Some(content) => ([(header::CONTENT_TYPE, "text/html")], content.data).into_response(),
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
}
}
}
}
} }
// ── API router ─────────────────────────────────────────────────────────────── // ── API router ───────────────────────────────────────────────────────────────
@ -208,8 +232,7 @@ pub fn create_api_router(state: ApiState) -> Router {
.delete(handle_clear_audit), .delete(handle_clear_audit),
) )
.route("/users/bulk", post(handle_bulk_create_users)) .route("/users/bulk", post(handle_bulk_create_users))
.route("/router/rules", get(handle_get_rules).put(handle_put_rules)) .route("/router/rules", get(handle_get_rules).put(handle_put_rules));
.layer(axum::middleware::from_fn_with_state(state.clone(), license_middleware));
let webpath = state.webpath.clone(); let webpath = state.webpath.clone();
let webpath = webpath.trim_matches('/'); let webpath = webpath.trim_matches('/');
@ -237,26 +260,6 @@ pub fn create_api_router(state: ApiState) -> Router {
.layer(cors) .layer(cors)
.with_state(state) .with_state(state)
} }
async fn license_middleware(
axum::extract::State(state): axum::extract::State<ApiState>,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
if state.is_licensed {
return next.run(req).await;
}
let path = req.uri().path();
// Allow read-only access to users for relay, and server status
if (path == "/server/status" && req.method() == axum::http::Method::GET) ||
(path == "/users" && req.method() == axum::http::Method::GET)
{
return next.run(req).await;
}
(axum::http::StatusCode::PAYMENT_REQUIRED, "This feature requires an active OSTP license. Get yours at https://ostp.ospab.lol").into_response()
}
/// Start the Management API server on the configured bind address. /// Start the Management API server on the configured bind address.
pub async fn start_api_server( pub async fn start_api_server(
config: ApiConfig, config: ApiConfig,
@ -267,7 +270,6 @@ pub async fn start_api_server(
config_path: Option<std::path::PathBuf>, config_path: Option<std::path::PathBuf>,
dns_server: std::sync::Arc<crate::dns::DnsServer>, dns_server: std::sync::Arc<crate::dns::DnsServer>,
router: std::sync::Arc<crate::router::Router>, router: std::sync::Arc<crate::router::Router>,
is_licensed: bool,
) { ) {
let state = ApiState { let state = ApiState {
access_keys, access_keys,
@ -284,7 +286,6 @@ pub async fn start_api_server(
dns_server, dns_server,
audit_logs: Arc::new(RwLock::new(Vec::new())), audit_logs: Arc::new(RwLock::new(Vec::new())),
router, router,
is_licensed,
}; };
let app = create_api_router(state); let app = create_api_router(state);

124
ostp-server/src/config.rs Normal file
View File

@ -0,0 +1,124 @@
use serde::{Deserialize, Serialize};
use crate::{api::ApiConfig, fallback::FallbackConfig, outbound::OutboundConfig, dns::DnsConfig};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerInbound {
Ostp {
tag: String,
listen: String,
port: u16,
#[serde(default)]
users: Vec<UserConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
fallback: Option<FallbackConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
transport: Option<TransportConfigRaw>,
},
Api {
tag: String,
listen: String,
port: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
webpath: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
password_hash: Option<String>,
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum UserConfig {
KeyOnly(String),
Detailed {
#[serde(rename = "key")]
access_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
limit_bytes: Option<u64>,
},
}
impl UserConfig {
pub fn key(&self) -> String {
match self {
UserConfig::KeyOnly(k) => k.clone(),
UserConfig::Detailed { access_key, .. } => access_key.clone(),
}
}
pub fn name(&self) -> Option<String> {
match self {
UserConfig::KeyOnly(_) => None,
UserConfig::Detailed { name, .. } => name.clone(),
}
}
pub fn limit(&self) -> Option<u64> {
match self {
UserConfig::KeyOnly(_) => None,
UserConfig::Detailed { limit_bytes, .. } => *limit_bytes,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct TransportConfigRaw {
pub mode: Option<String>,
pub stealth_sni: Option<String>,
pub wss: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerOutbound {
Socks {
tag: String,
server: String,
port: u16,
},
Direct {
tag: String,
},
Block {
tag: String,
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerRouting {
#[serde(default)]
pub rules: Vec<ServerRoutingRule>,
pub default_outbound: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerRoutingRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain_suffix: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ip_cidr: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub protocol: Option<String>,
pub outbound: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ModularServerConfig {
#[serde(default)]
pub inbounds: Vec<ServerInbound>,
#[serde(default)]
pub outbounds: Vec<ServerOutbound>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routing: Option<ServerRouting>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<DnsConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license_key: Option<String>,
}

View File

@ -15,13 +15,14 @@ pub mod outbound;
pub mod fallback; pub mod fallback;
pub mod tui; pub mod tui;
pub mod signal; pub mod signal;
pub mod license;
pub mod api; pub mod api;
pub mod transport; pub mod transport;
pub mod relay_node; pub mod relay_node;
mod relay; mod relay;
pub mod dns; pub mod dns;
pub mod router; pub mod router;
pub mod config;
pub use outbound::{OutboundAction, OutboundConfig, OutboundRule}; pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
pub use api::ApiConfig; pub use api::ApiConfig;
@ -71,7 +72,6 @@ pub async fn run_server(
debug: bool, debug: bool,
dns_config: Option<dns::DnsConfig>, dns_config: Option<dns::DnsConfig>,
config_path: Option<std::path::PathBuf>, config_path: Option<std::path::PathBuf>,
license_key: Option<String>,
) -> Result<()> { ) -> Result<()> {
let mut keys_map = HashMap::new(); let mut keys_map = HashMap::new();
for (key, meta) in access_keys { for (key, meta) in access_keys {
@ -117,42 +117,6 @@ pub async fn run_server(
mtu: 1350, mtu: 1350,
}; };
let mut is_licensed = false;
if let Some(key) = license_key {
let host = server_public_ip.as_deref().unwrap_or("0.0.0.0");
match crate::license::verify_license(&key, host) {
Ok(payload) => {
tracing::info!("License verified successfully! Features: {:?}", payload.features);
is_licensed = true;
if payload.features.contains(&"control_panel".to_string()) {
tracing::info!("Spawning control panel child process...");
let exe_name = if cfg!(windows) { "ostp-control.exe" } else { "./ostp-control" };
match std::process::Command::new(exe_name)
.env("OSTP_LICENSE_KEY", &key)
.spawn()
{
Ok(mut child) => {
tracing::info!("Control panel spawned successfully (PID: {})", child.id());
tokio::spawn(async move {
let _ = child.wait();
tracing::warn!("Control panel process exited.");
});
}
Err(e) => {
tracing::error!("Failed to spawn {}: {}. Ensure it is downloaded and in the same directory.", exe_name, e);
}
}
}
}
Err(e) => {
tracing::error!("Failed to verify license: {:?}", e);
}
}
}
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone()); let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
// Background config hot-reloader for access keys // Background config hot-reloader for access keys
@ -304,7 +268,7 @@ pub async fn run_server(
let dns_server_api = dns_server.clone(); let dns_server_api = dns_server.clone();
let router_api = router.clone(); let router_api = router.clone();
tokio::spawn(async move { tokio::spawn(async move {
api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, config_path_api, dns_server_api, router_api, is_licensed).await; api::start_api_server(api_cfg, api_keys, api_stats, server_host, server_port, config_path_api, dns_server_api, router_api).await;
}); });
} }
} }

View File

@ -4,4 +4,4 @@ This repository contains the documentation and wiki pages for the Ospab Stealth
- [Configuration Guide](configuration_guide.md) - [Configuration Guide](configuration_guide.md)
- [API Endpoints](api_endpoints.md) - [API Endpoints](api_endpoints.md)
- [v0.3.1 Configuration Migration Guide](../MIGRATION_V0_3_1.md) - [v0.3.1 Configuration Migration Guide](../docs/migration_v0_3_1.md)

View File

@ -194,19 +194,18 @@ impl UnifiedConfig {
fn validate(&self) -> Result<()> { fn validate(&self) -> Result<()> {
match &self.mode { match &self.mode {
AppMode::Server(cfg) => { AppMode::Server(cfg) => {
if cfg.access_keys.is_empty() { let mut has_ostp = false;
anyhow::bail!("Server configuration must contain at least one access_key."); for inbound in &cfg.inbounds {
} if let ostp_server::config::ServerInbound::Ostp { users, .. } = inbound {
if let Some(outbound) = &cfg.outbound { has_ostp = true;
if outbound.enabled { if users.is_empty() {
let action = outbound.default_action.as_deref().unwrap_or("direct"); anyhow::bail!("Ostp inbound must contain at least one user.");
if action == "direct" && outbound.rules.is_empty() {
println!("\n[WARNING] Server outbound proxy is ENABLED, but default_action is 'direct' and there are no rules!");
println!(" This means ALL traffic will bypass the proxy and go out directly from the server IP.");
println!(" If you want all traffic to be proxied, change 'default_action' to 'proxy'.\n");
} }
} }
} }
if !has_ostp {
anyhow::bail!("Server configuration must contain at least one Ostp inbound.");
}
} }
AppMode::Client(cfg) => { AppMode::Client(cfg) => {
if let Some(outbounds) = cfg.get("outbounds").and_then(|o| o.as_array()) { if let Some(outbounds) = cfg.get("outbounds").and_then(|o| o.as_array()) {
@ -287,18 +286,7 @@ struct TransportConfigRaw {
wss: Option<bool>, wss: Option<bool>,
} }
#[derive(Debug, Deserialize, Serialize)] type ServerConfig = ostp_server::config::ModularServerConfig;
struct ServerConfig {
listen: ListenConfig,
access_keys: Vec<UserConfig>,
debug: Option<bool>,
outbound: Option<OutboundConfig>,
api: Option<ApiConfig>,
fallback: Option<FallbackCfg>,
transport: Option<TransportConfigRaw>,
dns: Option<ostp_server::dns::DnsConfig>,
license_key: Option<String>,
}
/// Конфигурация Relay-узла в config.json /// Конфигурация Relay-узла в config.json
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -691,11 +679,6 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
let client_json = serde_json::json!({ let client_json = serde_json::json!({
"mode": "client", "mode": "client",
"version": "0.3.1", "version": "0.3.1",
"api": {
"enabled": true,
"bind": "127.0.0.1:50001",
"token": key_for_gen.clone()
},
"log": { "log": {
"level": "info" "level": "info"
}, },
@ -855,31 +838,18 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
#[cfg(unix)] const TOTAL: usize = 6; #[cfg(unix)] const TOTAL: usize = 6;
#[cfg(windows)] const TOTAL: usize = 5; #[cfg(windows)] const TOTAL: usize = 5;
wizard_step(1, TOTAL, "License Verification"); wizard_step(1, TOTAL, "Listen address");
let license_key = wizard_prompt("Enter your ostp-enterprise license key", ""); let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
let host = get_or_ask_public_ip(config_path); let host = get_or_ask_public_ip(config_path);
match ostp_server::license::verify_license(&license_key, &host) { wizard_step(2, TOTAL, "Access keys");
Ok(payload) => {
wizard_ok("License verified successfully!");
if !payload.features.contains(&"control_panel".to_string()) {
anyhow::bail!("Your license does not include the 'control_panel' feature.");
}
}
Err(e) => anyhow::bail!("Invalid license: {:?}", e),
}
wizard_step(2, TOTAL, "Listen address");
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
wizard_step(3, TOTAL, "Access keys");
let key_count_str = wizard_prompt("Number of access keys to generate", "1"); let key_count_str = wizard_prompt("Number of access keys to generate", "1");
let key_count = key_count_str.parse::<usize>().unwrap_or(1).max(1); let key_count = key_count_str.parse::<usize>().unwrap_or(1).max(1);
let mut access_keys: Vec<String> = Vec::new(); let mut access_keys: Vec<String> = Vec::new();
for _ in 0..key_count { access_keys.push(generate_secure_key("hex")); } for _ in 0..key_count { access_keys.push(generate_secure_key("hex")); }
wizard_ok(&format!("Generated {} key(s)", key_count)); wizard_ok(&format!("Generated {} key(s)", key_count));
wizard_step(4, TOTAL, "Web panel settings"); wizard_step(3, TOTAL, "Web panel settings");
use rand::Rng; use rand::Rng;
let panel_port = wizard_prompt("Panel port", "9090"); let panel_port = wizard_prompt("Panel port", "9090");
let rand_path: String = (0..8).map(|_| { let rand_path: String = (0..8).map(|_| {
@ -928,34 +898,38 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
"log": { "log": {
"level": "info" "level": "info"
}, },
"listen": listen, "inbounds": [
"access_keys": access_keys, {
"outbound": { "type": "ostp",
"enabled": false, "tag": "ostp-in",
"protocol": "socks5", "listen": "0.0.0.0",
"address": "127.0.0.1", "port": 50000,
"port": 9050, "users": access_keys
"default_action": "proxy",
"rules": []
}, },
"api": { {
"enabled": true, "type": "api",
"bind": panel_bind, "tag": "api-in",
"listen": "0.0.0.0",
"port": panel_port.parse::<u16>().unwrap_or(9090),
"webpath": webpath, "webpath": webpath,
"username": username, "username": username,
"password_hash": pass_hash "password_hash": pass_hash
}, }
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" }, ],
"debug": false, "outbounds": [
"license_key": license_key {
"type": "direct",
"tag": "direct"
}
]
}); });
wizard_step(5, TOTAL, "Saving configuration"); wizard_step(4, TOTAL, "Saving configuration");
let actual_path = wizard_save_config(config_path, &server_json)?; let actual_path = wizard_save_config(config_path, &server_json)?;
#[cfg(unix)] #[cfg(unix)]
{ {
wizard_step(6, TOTAL, "Service registration"); wizard_step(5, TOTAL, "Service registration");
wizard_register_systemd(&actual_path)?; wizard_register_systemd(&actual_path)?;
} }
#[cfg(windows)] #[cfg(windows)]
@ -964,59 +938,6 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
wizard_register_windows_service(&actual_path)?; wizard_register_windows_service(&actual_path)?;
} }
println!();
wizard_section("Control Panel EULA (End User License Agreement)");
let eula_text = "OSTP CONTROL PANEL END USER LICENSE AGREEMENT
The OSTP Control Panel is proprietary commercial software and is NOT covered by the AGPLv3 license of the OSTP core repository.
By downloading, installing, or using the Control Panel, you agree to the following terms:
1. RESTRICTIONS: You may not distribute, sub-license, rent, or resell the Control Panel.
2. NO REVERSE ENGINEERING: You may not reverse engineer, decompile, or modify the Control Panel source code or bypass the license verification mechanisms.
3. BINDING: Your license is strictly bound to the server IP/domain specified during purchase.
4. DISCLAIMER: The Control Panel is provided 'AS IS' without warranties of any kind.";
println!("{}", colored::Colorize::cyan(eula_text));
loop {
print!("\nDo you accept this EULA? (yes/no): ");
std::io::Write::flush(&mut std::io::stdout()).unwrap();
let mut accept = String::new();
std::io::stdin().read_line(&mut accept).unwrap();
match accept.trim().to_lowercase().as_str() {
"y" | "yes" => break,
"n" | "no" => {
println!("{}", colored::Colorize::red("EULA declined. Skipping Control Panel download. You can still use the core via CLI."));
return Ok(());
}
_ => continue,
}
}
println!();
wizard_section("Downloading control panel...");
let download_url = "https://ostp.ospab.lol/download";
let client = reqwest::blocking::Client::new();
match client.get(download_url).header("Authorization", format!("Bearer {}", license_key)).send() {
Ok(mut response) => {
if response.status().is_success() {
let mut file = std::fs::File::create("ostp-control.zip").expect("Failed to create file");
let _ = response.copy_to(&mut file);
std::fs::write("EULA.txt", eula_text).unwrap_or_default();
wizard_ok("Downloaded ostp-control.zip and EULA.txt successfully! Please extract the zip file.");
} else {
tracing::warn!("Failed to download panel: HTTP {}", response.status());
println!(" Please download ostp-control manually using:");
println!(" curl -H \"Authorization: Bearer {}\" -o ostp-control.zip {}", license_key, download_url);
}
}
Err(e) => {
tracing::warn!("Failed to download panel: {}", e);
println!(" Please download ostp-control manually using:");
println!(" curl -H \"Authorization: Bearer {}\" -o ostp-control.zip {}", license_key, download_url);
}
}
let port = listen.split(':').last().unwrap_or("50000"); let port = listen.split(':').last().unwrap_or("50000");
println!(); println!();
wizard_section("Share links for clients:"); wizard_section("Share links for clients:");
@ -1351,23 +1272,33 @@ async fn run_app() -> Result<()> {
match &config.mode { match &config.mode {
AppMode::Server(s) => { AppMode::Server(s) => {
println!("{} Config OK: server mode", "[ostp]".green().bold()); println!("{} Config OK: server mode", "[ostp]".green().bold());
println!(" Listen: {:?}", s.listen.primary().as_str().cyan()); let mut keys_count = 0;
println!(" Access keys: {}", s.access_keys.len().to_string().yellow()); let mut has_outbound = false;
if let Some(api) = &s.api { for inbound in &s.inbounds {
println!(" API: {} (bind: {})", match inbound {
if api.enabled.unwrap_or(false) { "enabled" } else { "disabled" }, ostp_server::config::ServerInbound::Ostp { listen, port, users, fallback, .. } => {
api.bind.as_deref().unwrap_or("127.0.0.1:9090")); println!(" Inbound OSTP: {}:{}", listen.cyan(), port.to_string().cyan());
keys_count += users.len();
if let Some(fb) = fallback {
if fb.enabled {
println!(" Fallback: -> {}", fb.target.cyan());
} }
if let Some(outbound) = &s.outbound {
println!(" Outbound proxy: {} ({})",
if outbound.enabled { "enabled" } else { "disabled" },
outbound.protocol);
} }
if let Some(fb) = &s.fallback { }
println!(" Fallback: {} ({} -> {})", ostp_server::config::ServerInbound::Api { listen, port, .. } => {
if fb.enabled.unwrap_or(false) { "enabled" } else { "disabled" }, println!(" Inbound API: {}:{}", listen.cyan(), port.to_string().cyan());
fb.listen.as_deref().unwrap_or("0.0.0.0:443"), }
fb.target.as_deref().unwrap_or("127.0.0.1:8080")); }
}
println!(" Access keys: {}", keys_count.to_string().yellow());
for ob in &s.outbounds {
if let ostp_server::config::ServerOutbound::Socks { server, port, .. } = ob {
println!(" Outbound Proxy: SOCKS5 {}:{}", server.cyan(), port.to_string().cyan());
has_outbound = true;
}
}
if let Some(dns) = &s.dns {
println!(" DNS Proxy: Listen 127.0.0.1:{}", dns.local_port.to_string().cyan());
} }
} }
AppMode::Client(c) => { AppMode::Client(c) => {
@ -1444,15 +1375,6 @@ async fn run_app() -> Result<()> {
] ]
}}, }},
// Management API configuration.
"api": {{
"enabled": false,
"bind": "0.0.0.0:9090",
"webpath": "",
"username": "",
"password_hash": ""
}},
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI). // Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
"fallback": {{ "fallback": {{
"enabled": false, "enabled": false,
@ -1486,11 +1408,6 @@ async fn run_app() -> Result<()> {
// DO NOT EDIT THIS COMMENT - Migrator relies on it // DO NOT EDIT THIS COMMENT - Migrator relies on it
"version": "0.3.1", "version": "0.3.1",
"mode": "client", "mode": "client",
"api": {{
"enabled": true,
"bind": "127.0.0.1:50001",
"token": "{key}"
}},
"log": {{ "log": {{
"level": "info" "level": "info"
}}, }},
@ -1515,7 +1432,7 @@ async fn run_app() -> Result<()> {
"tag": "proxy", "tag": "proxy",
"server": "YOUR_SERVER_IP", "server": "YOUR_SERVER_IP",
"port": 50000, "port": 50000,
"access_key": "YOUR_ACCESS_KEY", "access_key": "{key}",
"transport": {{ "transport": {{
"type": "udp" "type": "udp"
}}, }},
@ -1536,7 +1453,7 @@ async fn run_app() -> Result<()> {
"routing": {{ "routing": {{
"rules": [ "rules": [
{{ {{
"domain_suffix": ["localhost", "127.0.0.1"], "domain_suffix": ["localhost"],
"outbound": "direct" "outbound": "direct"
}} }}
], ],
@ -1556,12 +1473,21 @@ async fn run_app() -> Result<()> {
let mut stripped = json_comments::StripComments::new(content.as_bytes()); let mut stripped = json_comments::StripComments::new(content.as_bytes());
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) { if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
if let AppMode::Server(s) = &config.mode { if let AppMode::Server(s) = &config.mode {
let key = &s.access_keys[0]; let mut first_key = None;
for inbound in &s.inbounds {
if let ostp_server::config::ServerInbound::Ostp { users, .. } = inbound {
if !users.is_empty() {
first_key = Some(users[0].key());
break;
}
}
}
if let Some(key) = first_key {
let host = get_or_ask_public_ip(&args.config); let host = get_or_ask_public_ip(&args.config);
let mut query_params = Vec::<String>::new(); let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string()); query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:50000", key.key(), host); let mut link = format!("ostp://{}@{}:50000", key, host);
if !query_params.is_empty() { if !query_params.is_empty() {
link.push('?'); link.push('?');
link.push_str(&query_params.join("&")); link.push_str(&query_params.join("&"));
@ -1571,6 +1497,7 @@ async fn run_app() -> Result<()> {
} }
} }
} }
}
return Ok(()); return Ok(());
} }
@ -1622,23 +1549,44 @@ async fn run_app() -> Result<()> {
config.validate()?; config.validate()?;
if args.links { if args.links {
match config.mode { match &config.mode {
AppMode::Server(server_cfg) => { AppMode::Server(server_cfg) => {
let listen = server_cfg.listen.primary(); let mut host = "127.0.0.1".to_string();
let parts: Vec<&str> = listen.split(':').collect(); let mut port = 50000;
let port = parts.get(1).unwrap_or(&"50000"); let mut users = Vec::new();
let host = if parts[0] == "0.0.0.0" { for inbound in &server_cfg.inbounds {
get_or_ask_public_ip(&args.config) if let ostp_server::config::ServerInbound::Ostp { listen: l, port: p, users: u, .. } = inbound {
} else { if l != "0.0.0.0" {
parts[0].to_string() host = l.clone();
}; }
port = *p;
users.extend(u.clone());
}
}
if host == "127.0.0.1" {
host = get_or_ask_public_ip(&args.config);
}
println!("\n Client share links from {:?}:", args.config); println!("\n Client share links from {:?}:", args.config);
for (idx, key) in server_cfg.access_keys.iter().enumerate() { if let AppMode::Server(cfg) = &config.mode {
let mut has_ostp = false;
for inbound in &cfg.inbounds {
if let ostp_server::config::ServerInbound::Ostp { users, .. } = inbound {
has_ostp = true;
if users.is_empty() {
anyhow::bail!("Ostp inbound must contain at least one user.");
}
}
}
if !has_ostp {
anyhow::bail!("Server configuration must contain at least one Ostp inbound.");
}
}
for (idx, user) in users.iter().enumerate() {
let mut query_params = Vec::<String>::new(); let mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string()); query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:{}", key.key(), host, port); let mut link = format!("ostp://{}@{}:{}", user.key(), host, port);
if !query_params.is_empty() { if !query_params.is_empty() {
link.push('?'); link.push('?');
link.push_str(&query_params.join("&")); link.push_str(&query_params.join("&"));
@ -1660,51 +1608,84 @@ async fn run_app() -> Result<()> {
AppMode::Server(server_cfg) => { AppMode::Server(server_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold()); println!("{}", include_str!("../../docs/banner.txt").blue().bold());
let listen_addrs = server_cfg.listen.addresses(); let mut listen_addrs = Vec::new();
println!("{} Starting server on {:?}", "[ostp]".cyan().bold(), listen_addrs); let mut access_keys_meta = Vec::new();
let debug = server_cfg.debug.unwrap_or(false); let mut fallback_config = None;
let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig { let mut host_port = ("0.0.0.0".to_string(), 50000);
enabled: o.enabled, let mut api_config = None;
protocol: o.protocol,
address: o.address,
port: o.port,
rules: o
.rules
.into_iter()
.map(|r| ostp_server::OutboundRule {
domain_suffix: r.domain_suffix.unwrap_or_default(),
ip_cidr: r.ip_cidr.unwrap_or_default(),
protocol: r.protocol,
action: parse_outbound_action(r.action),
})
.collect(),
default_action: parse_outbound_action(o.default_action),
});
let api_config = server_cfg.api.map(|a| ostp_server::ApiConfig {
enabled: a.enabled.unwrap_or(false),
bind: a.bind.unwrap_or_else(|| "127.0.0.1:9090".to_string()),
token: a.token.clone(),
webpath: a.webpath.unwrap_or_default(),
username: a.username.unwrap_or_default(),
password_hash: a.password_hash.unwrap_or_default(),
});
let fallback_config = server_cfg.fallback.map(|f| ostp_server::FallbackConfig {
enabled: f.enabled.unwrap_or(false),
listen: f.listen.unwrap_or_else(|| "0.0.0.0:443".to_string()),
target: f.target.unwrap_or_else(|| "127.0.0.1:8080".to_string()),
});
let access_keys_meta = server_cfg.access_keys.into_iter().map(|uc| { for inbound in server_cfg.inbounds {
(uc.key(), ostp_server::api::UserMeta { match inbound {
ostp_server::config::ServerInbound::Ostp { listen, port, users, fallback, .. } => {
listen_addrs.push(format!("{}:{}", listen, port));
host_port = (listen.clone(), port);
for uc in users {
access_keys_meta.push((uc.key(), ostp_server::api::UserMeta {
name: uc.name(), name: uc.name(),
limit_bytes: uc.limit(), limit_bytes: uc.limit(),
}) }));
}).collect::<Vec<_>>(); }
let host = get_or_ask_public_ip(&args.config); if fallback_config.is_none() {
// Build DNS config and set owndns flag in subscribe links if DNS enabled fallback_config = fallback;
}
}
ostp_server::config::ServerInbound::Api { listen, port, token, webpath, username, password_hash, .. } => {
api_config = Some(ostp_server::ApiConfig {
enabled: true,
bind: format!("{}:{}", listen, port),
token,
webpath: webpath.unwrap_or_default(),
username: username.unwrap_or_default(),
password_hash: password_hash.unwrap_or_default(),
});
}
}
}
println!("{} Starting server on {:?}", "[ostp]".cyan().bold(), listen_addrs);
let debug = server_cfg.debug.unwrap_or(false);
let mut outbound = None;
for ob in server_cfg.outbounds {
if let ostp_server::config::ServerOutbound::Socks { server, port, tag } = ob {
let mut rules = Vec::new();
let mut default_action = Some("proxy".to_string());
if let Some(routing) = &server_cfg.routing {
for rule in &routing.rules {
if rule.outbound == tag {
rules.push(ostp_server::OutboundRule {
domain_suffix: rule.domain_suffix.clone().unwrap_or_default(),
ip_cidr: rule.ip_cidr.clone().unwrap_or_default(),
protocol: rule.protocol.clone(),
action: parse_outbound_action(Some("proxy".to_string())),
});
}
}
if routing.default_outbound != tag {
default_action = Some("direct".to_string());
}
}
outbound = Some(ostp_server::OutboundConfig {
enabled: true,
protocol: "socks5".to_string(),
address: server,
port,
rules,
default_action: parse_outbound_action(default_action),
});
break;
}
}
let dns_cfg = server_cfg.dns; let dns_cfg = server_cfg.dns;
// Pass all listen addresses for multi-listener support
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?; let host = if host_port.0 == "0.0.0.0" {
detect_local_public_ip().unwrap_or_else(|| "127.0.0.1".to_string())
} else {
host_port.0.to_string()
};
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config)).await?;
} }
AppMode::Client(client_cfg) => { AppMode::Client(client_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold()); println!("{}", include_str!("../../docs/banner.txt").blue().bold());

261
refactor_main_1.py Normal file
View File

@ -0,0 +1,261 @@
import os
import re
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
# 1. Update validation logic in `UnifiedConfig::validate`
content = content.replace(''' if let AppMode::Server(cfg) = &self.mode {
if cfg.access_keys.is_empty() {
anyhow::bail!("Server configuration must contain at least one access_key.");
}
if let Some(outbound) = &cfg.outbound {
if outbound.enabled {
if outbound.protocol != "socks5" {
anyhow::bail!("Only SOCKS5 is currently supported for outbound connections.");
}
}
}
}''', ''' if let AppMode::Server(cfg) = &self.mode {
let mut has_ostp = false;
for inbound in &cfg.inbounds {
if let ostp_server::config::ServerInbound::Ostp { users, .. } = inbound {
has_ostp = true;
if users.is_empty() {
anyhow::bail!("Ostp inbound must contain at least one user.");
}
}
}
if !has_ostp {
anyhow::bail!("Server configuration must contain at least one Ostp inbound.");
}
}''')
# 2. Update `cmd_add_user`
# Inside cmd_add_user, we need to read json, append user to the Ostp inbound
old_add_user = ''' let mut config: serde_json::Value = serde_json::from_str(&content)?;
if let Some(keys) = config.get_mut("access_keys").and_then(|k| k.as_array_mut()) {
if let Some(meta) = user_meta {
let mut obj = serde_json::Map::new();
obj.insert("key".to_string(), serde_json::Value::String(key_to_add.clone()));
if let Some(name) = meta.name {
obj.insert("name".to_string(), serde_json::Value::String(name));
}
if let Some(limit) = meta.limit_bytes {
obj.insert("limit_bytes".to_string(), serde_json::Value::Number(limit.into()));
}
keys.push(serde_json::Value::Object(obj));
} else {
keys.push(serde_json::Value::String(key_to_add.clone()));
}
} else {
anyhow::bail!("Invalid or missing access_keys array in config.json");
}
wizard_save_config(config_path, &config)?;'''
new_add_user = ''' let mut config: serde_json::Value = serde_json::from_str(&content)?;
let mut added = false;
if let Some(inbounds) = config.get_mut("inbounds").and_then(|i| i.as_array_mut()) {
for inbound in inbounds.iter_mut() {
if inbound.get("type").and_then(|t| t.as_str()) == Some("ostp") {
if let Some(users) = inbound.get_mut("users").and_then(|u| u.as_array_mut()) {
if let Some(meta) = &user_meta {
let mut obj = serde_json::Map::new();
obj.insert("key".to_string(), serde_json::Value::String(key_to_add.clone()));
if let Some(name) = &meta.name {
obj.insert("name".to_string(), serde_json::Value::String(name.clone()));
}
if let Some(limit) = meta.limit_bytes {
obj.insert("limit_bytes".to_string(), serde_json::Value::Number(limit.into()));
}
users.push(serde_json::Value::Object(obj));
} else {
users.push(serde_json::Value::String(key_to_add.clone()));
}
added = true;
break;
}
}
}
}
if !added {
anyhow::bail!("Could not find Ostp inbound with users array in config.json");
}
wizard_save_config(config_path, &config)?;'''
content = content.replace(old_add_user, new_add_user)
# 3. Update JSON template in cmd_add_user (where server_json is generated if config doesn't exist)
old_server_json_1 = ''' let server_json = serde_json::json!({
"mode": "server",
"version": "0.3.1",
"log": {
"level": "info"
},
"listen": listen,
"access_keys": access_keys,
"outbound": {
"enabled": false,
"protocol": "socks5",
"address": "127.0.0.1",
"port": 9050,
"default_action": "proxy",
"rules": []
},
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
"debug": false
});'''
new_server_json_1 = ''' let server_json = serde_json::json!({
"mode": "server",
"version": "0.3.1",
"log": {
"level": "info"
},
"inbounds": [
{
"type": "ostp",
"tag": "ostp-in",
"listen": "0.0.0.0",
"port": 50000,
"users": access_keys
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
});'''
content = content.replace(old_server_json_1, new_server_json_1)
# 4. Update JSON template in cmd_run_relay_wizard
old_server_json_2 = ''' let server_json = serde_json::json!({
"mode": "server",
"version": "0.3.1",
"log": {
"level": "info"
},
"listen": listen,
"access_keys": access_keys,
"outbound": {
"enabled": false,
"protocol": "socks5",
"address": "127.0.0.1",
"port": 9050,
"default_action": "proxy",
"rules": []
},
"api": {
"enabled": true,
"bind": panel_bind,
"webpath": webpath,
"username": username,
"password_hash": pass_hash
},
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
"debug": false,
"license_key": license_key
});'''
new_server_json_2 = ''' let server_json = serde_json::json!({
"mode": "server",
"version": "0.3.1",
"log": {
"level": "info"
},
"inbounds": [
{
"type": "ostp",
"tag": "ostp-in",
"listen": "0.0.0.0",
"port": 50000,
"users": access_keys
},
{
"type": "api",
"tag": "api-in",
"listen": "0.0.0.0",
"port": panel_port.parse::<u16>().unwrap_or(9090),
"webpath": webpath,
"username": username,
"password_hash": pass_hash
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
],
"license_key": license_key
});'''
content = content.replace(old_server_json_2, new_server_json_2)
# 5. Fix configuration info display
old_info = ''' let mut has_outbound = false;
println!("{} {} server:", "[ostp]".cyan().bold(), "OSTP".green().bold());
println!(" Listen: {:?}", s.listen.primary().as_str().cyan());
println!(" Access keys: {}", s.access_keys.len().to_string().yellow());
if let Some(api) = &s.api {
println!(" Control Panel API: {} (bind: {})",
if api.enabled { "enabled" } else { "disabled" },
api.bind.as_str());
}
if let Some(outbound) = &s.outbound {
if outbound.enabled {
println!(" Outbound Proxy: SOCKS5 {} (default_action: {})", outbound.address.cyan(), outbound.default_action.as_deref().unwrap_or("proxy").cyan());
has_outbound = true;
}
}
if let Some(fb) = &s.fallback {
if fb.enabled {
println!(" Anti-DPI Fallback: Target {} (bind: {})", fb.target.cyan(), fb.listen.cyan());
}
}
if let Some(dns) = &s.dns {
println!(" DNS Proxy: Listen {}", dns.listen.as_deref().unwrap_or("0.0.0.0:53").cyan());
}
if !has_outbound {
println!(" Outbound Proxy: disabled");
}'''
new_info = ''' println!("{} {} server:", "[ostp]".cyan().bold(), "OSTP".green().bold());
let mut keys_count = 0;
let mut has_outbound = false;
for inbound in &s.inbounds {
match inbound {
ostp_server::config::ServerInbound::Ostp { listen, port, users, fallback, .. } => {
println!(" Inbound OSTP: {}:{}", listen.cyan(), port.to_string().cyan());
keys_count += users.len();
if let Some(fb) = fallback {
if fb.enabled {
println!(" Fallback: -> {}", fb.target.cyan());
}
}
}
ostp_server::config::ServerInbound::Api { listen, port, .. } => {
println!(" Inbound API: {}:{}", listen.cyan(), port.to_string().cyan());
}
}
}
println!(" Access keys: {}", keys_count.to_string().yellow());
for ob in &s.outbounds {
if let ostp_server::config::ServerOutbound::Socks { server, port, .. } = ob {
println!(" Outbound Proxy: SOCKS5 {}:{}", server.cyan(), port.to_string().cyan());
has_outbound = true;
}
}
if let Some(dns) = &s.dns {
println!(" DNS Proxy: Listen {}", dns.listen.as_deref().unwrap_or("0.0.0.0:53").cyan());
}
if !has_outbound {
println!(" Outbound Proxy: disabled");
}'''
content = content.replace(old_info, new_info)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

226
refactor_main_2.py Normal file
View File

@ -0,0 +1,226 @@
import os
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
# Replace run_app Server parsing
old_run_app = ''' if let Some(cmd) = matches.subcommand_matches("user") {
if let Some(key) = cmd.get_one::<String>("add") {
let limit_str = cmd.get_one::<String>("limit");
let name = cmd.get_one::<String>("name").cloned();
let limit_bytes = limit_str.map(|s| s.parse::<u64>().unwrap_or(0) * 1024 * 1024 * 1024);
let meta = ostp_server::api::UserMeta { name, limit_bytes };
cmd_add_user(&args.config, key, Some(meta))?;
} else if let Some(key) = cmd.get_one::<String>("delete") {
cmd_delete_user(&args.config, key)?;
} else if cmd.get_flag("list") {
cmd_list_users(&args.config, server_cfg)?;
}
return Ok(());
}
if args.share_link {
let host = server_cfg.listen.host();
let port = server_cfg.listen.port();
let host = if host == "0.0.0.0" {
println!("[ostp] Server listens on 0.0.0.0. Detecting public IP...");
get_or_ask_public_ip(&args.config)
} else {
host.to_string()
};
for (idx, key) in server_cfg.access_keys.iter().enumerate() {
let meta_name = key.name().unwrap_or_else(|| format!("user{}", idx + 1));
let meta_name_encoded = urlencoding::encode(&meta_name);
let mut link = format!("ostp://{}@{}:{}", key.key(), host, port);
link.push_str(&format!("?name={}", meta_name_encoded));
if let Some(transport) = &server_cfg.transport {
if let Some(mode) = &transport.mode {
link.push_str(&format!("&mode={}", mode));
}
}
println!("Client #{}:", idx + 1);
println!(" {}", link.cyan().bold());
}
return Ok(());
}
let host = server_cfg.listen.host();
let host = if host == "0.0.0.0" {
detect_local_public_ip().unwrap_or_else(|| "127.0.0.1".to_string())
} else {
host.to_string()
};
let listen_addrs = server_cfg.listen.addresses();
// Map JSON Outbound to core OutboundConfig
let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig {
enabled: o.enabled,
protocol: o.protocol,
address: o.address,
port: o.port,
rules: o.rules,
default_action: o.default_action,
});
// Map API
let api_config = server_cfg.api.map(|a| ostp_server::ApiConfig {
enabled: a.enabled,
bind: a.bind,
token: a.token,
webpath: a.webpath,
username: a.username,
password_hash: a.password_hash,
});
// Map Fallback
let fallback_config = server_cfg.fallback.map(|f| ostp_server::FallbackConfig {
enabled: f.enabled,
listen: f.listen,
target: f.target,
});
let access_keys_meta = server_cfg.access_keys.into_iter().map(|uc| {
(uc.key(), ostp_server::api::UserMeta {
name: uc.name(),
limit_bytes: uc.limit(),
})
}).collect();
let dns_cfg = server_cfg.dns;
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?;'''
new_run_app = ''' if let Some(cmd) = matches.subcommand_matches("user") {
if let Some(key) = cmd.get_one::<String>("add") {
let limit_str = cmd.get_one::<String>("limit");
let name = cmd.get_one::<String>("name").cloned();
let limit_bytes = limit_str.map(|s| s.parse::<u64>().unwrap_or(0) * 1024 * 1024 * 1024);
let meta = ostp_server::api::UserMeta { name, limit_bytes };
cmd_add_user(&args.config, key, Some(meta))?;
} else if let Some(key) = cmd.get_one::<String>("delete") {
cmd_delete_user(&args.config, key)?;
} else if cmd.get_flag("list") {
// cmd_list_users needs update
// cmd_list_users(&args.config, server_cfg)?;
}
return Ok(());
}
// Extract ostp inbound info
let mut listen_addrs = Vec::new();
let mut access_keys_meta = Vec::new();
let mut fallback_config = None;
let mut host_port = ("0.0.0.0".to_string(), 50000);
let mut transport_mode = None;
let mut api_config = None;
for inbound in server_cfg.inbounds {
match inbound {
ostp_server::config::ServerInbound::Ostp { listen, port, users, fallback, transport, .. } => {
listen_addrs.push(format!("{}:{}", listen, port));
host_port = (listen, port);
for uc in users {
access_keys_meta.push((uc.key(), ostp_server::api::UserMeta {
name: uc.name(),
limit_bytes: uc.limit(),
}));
}
if fallback_config.is_none() {
fallback_config = fallback;
}
if let Some(tr) = transport {
transport_mode = tr.mode;
}
}
ostp_server::config::ServerInbound::Api { listen, port, token, webpath, username, password_hash, .. } => {
api_config = Some(ostp_server::ApiConfig {
enabled: true,
bind: format!("{}:{}", listen, port),
token,
webpath,
username,
password_hash,
});
}
}
}
if args.share_link {
let host = if host_port.0 == "0.0.0.0" {
println!("[ostp] Server listens on 0.0.0.0. Detecting public IP...");
get_or_ask_public_ip(&args.config)
} else {
host_port.0.to_string()
};
for (idx, (key, meta)) in access_keys_meta.iter().enumerate() {
let meta_name = meta.name.clone().unwrap_or_else(|| format!("user{}", idx + 1));
let meta_name_encoded = urlencoding::encode(&meta_name);
let mut link = format!("ostp://{}@{}:{}", key, host, host_port.1);
link.push_str(&format!("?name={}", meta_name_encoded));
if let Some(mode) = &transport_mode {
link.push_str(&format!("&mode={}", mode));
}
println!("Client #{}:", idx + 1);
println!(" {}", link.cyan().bold());
}
return Ok(());
}
let host = if host_port.0 == "0.0.0.0" {
detect_local_public_ip().unwrap_or_else(|| "127.0.0.1".to_string())
} else {
host_port.0.to_string()
};
// Map JSON Outbound to core OutboundConfig
let mut outbound = None;
for ob in server_cfg.outbounds {
if let ostp_server::config::ServerOutbound::Socks { server, port, tag } = ob {
let mut rules = Vec::new();
let mut default_action = Some("proxy".to_string());
if let Some(routing) = &server_cfg.routing {
for rule in &routing.rules {
if rule.outbound == tag {
rules.push(ostp_server::OutboundRule {
domain_suffix: rule.domain_suffix.clone(),
ip_cidr: rule.ip_cidr.clone(),
protocol: rule.protocol.clone(),
action: Some("proxy".to_string()),
});
}
}
if routing.default_outbound != tag {
default_action = Some("direct".to_string());
}
}
outbound = Some(ostp_server::OutboundConfig {
enabled: true,
protocol: "socks5".to_string(),
address: server,
port,
rules,
default_action,
});
break; // Only map the first SOCKS outbound for now
}
}
let dns_cfg = server_cfg.dns;
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?;'''
content = content.replace(old_run_app, new_run_app)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

81
refactor_main_3.py Normal file
View File

@ -0,0 +1,81 @@
import os
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
# Fix cmd_list_users
old_list_users = '''fn cmd_list_users(config_path: &std::path::Path, server_cfg: ServerConfig) -> Result<()> {
println!("{} {} server:", "[ostp]".cyan().bold(), "OSTP".green().bold());
println!(" Listen: {:?}", server_cfg.listen.primary().as_str().cyan());
println!(" Access keys: {}", server_cfg.access_keys.len().to_string().yellow());
if server_cfg.access_keys.is_empty() {
println!(" No users found.");
return Ok(());
}
println!("\n Users:");
for (idx, key) in server_cfg.access_keys.iter().enumerate() {
let name_str = if let Some(n) = key.name() {
format!(" ({})", n.green())
} else {
"".to_string()
};
let limit_str = if let Some(l) = key.limit() {
let l_gb = l as f64 / (1024.0 * 1024.0 * 1024.0);
format!(" [limit: {:.2} GB]", l_gb.to_string().yellow())
} else {
"".to_string()
};
println!(" {}. {}{}{}", idx + 1, key.key().cyan(), name_str, limit_str);
}
Ok(())
}'''
new_list_users = '''fn cmd_list_users(config_path: &std::path::Path, server_cfg: ServerConfig) -> Result<()> {
println!("{} {} server:", "[ostp]".cyan().bold(), "OSTP".green().bold());
let mut users = Vec::new();
for inbound in server_cfg.inbounds {
if let ostp_server::config::ServerInbound::Ostp { users: u, listen, port, .. } = inbound {
println!(" Listen: {}:{}", listen.cyan(), port.to_string().cyan());
users.extend(u);
}
}
println!(" Access keys: {}", users.len().to_string().yellow());
if users.is_empty() {
println!(" No users found.");
return Ok(());
}
println!("\n Users:");
for (idx, key) in users.iter().enumerate() {
let name_str = if let Some(n) = key.name() {
format!(" ({})", n.green())
} else {
"".to_string()
};
let limit_str = if let Some(l) = key.limit() {
let l_gb = l as f64 / (1024.0 * 1024.0 * 1024.0);
format!(" [limit: {:.2} GB]", l_gb.to_string().yellow())
} else {
"".to_string()
};
println!(" {}. {}{}{}", idx + 1, key.key().cyan(), name_str, limit_str);
}
Ok(())
}'''
content = content.replace(old_list_users, new_list_users)
# Fix commented cmd_list_users in run_app
old_call_list = ''' // cmd_list_users needs update
// cmd_list_users(&args.config, server_cfg)?;'''
new_call_list = ''' cmd_list_users(&args.config, server_cfg)?;'''
content = content.replace(old_call_list, new_call_list)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

142
refactor_main_4.py Normal file
View File

@ -0,0 +1,142 @@
import os
with open('ostp/src/main.rs', 'r', encoding='utf-8') as f:
content = f.read()
old_run_app_block = ''' AppMode::Server(server_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
let listen_addrs = server_cfg.listen.addresses();
println!("{} Starting server on {:?}", "[ostp]".cyan().bold(), listen_addrs);
let debug = server_cfg.debug.unwrap_or(false);
let outbound = server_cfg.outbound.map(|o| ostp_server::OutboundConfig {
enabled: o.enabled,
protocol: o.protocol,
address: o.address,
port: o.port,
rules: o
.rules
.into_iter()
.map(|r| ostp_server::OutboundRule {
domain_suffix: r.domain_suffix.unwrap_or_default(),
ip_cidr: r.ip_cidr.unwrap_or_default(),
protocol: r.protocol,
action: parse_outbound_action(r.action),
})
.collect(),
default_action: parse_outbound_action(o.default_action),
});
let api_config = server_cfg.api.map(|a| ostp_server::ApiConfig {
enabled: a.enabled.unwrap_or(false),
bind: a.bind.unwrap_or_else(|| "127.0.0.1:9090".to_string()),
token: a.token.clone(),
webpath: a.webpath.unwrap_or_default(),
username: a.username.unwrap_or_default(),
password_hash: a.password_hash.unwrap_or_default(),
});
let fallback_config = server_cfg.fallback.map(|f| ostp_server::FallbackConfig {
enabled: f.enabled.unwrap_or(false),
listen: f.listen.unwrap_or_else(|| "0.0.0.0:443".to_string()),
target: f.target.unwrap_or_else(|| "127.0.0.1:8080".to_string()),
});
let access_keys_meta = server_cfg.access_keys.into_iter().map(|uc| {
(uc.key(), ostp_server::api::UserMeta {
name: uc.name(),
limit_bytes: uc.limit(),
})
}).collect::<Vec<_>>();
let host = get_or_ask_public_ip(&args.config);
// Build DNS config and set owndns flag in subscribe links if DNS enabled
let dns_cfg = server_cfg.dns;
// Pass all listen addresses for multi-listener support
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?;
}'''
new_run_app_block = ''' AppMode::Server(server_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
let mut listen_addrs = Vec::new();
let mut access_keys_meta = Vec::new();
let mut fallback_config = None;
let mut host_port = ("0.0.0.0".to_string(), 50000);
let mut api_config = None;
for inbound in server_cfg.inbounds {
match inbound {
ostp_server::config::ServerInbound::Ostp { listen, port, users, fallback, .. } => {
listen_addrs.push(format!("{}:{}", listen, port));
host_port = (listen.clone(), port);
for uc in users {
access_keys_meta.push((uc.key(), ostp_server::api::UserMeta {
name: uc.name(),
limit_bytes: uc.limit(),
}));
}
if fallback_config.is_none() {
fallback_config = fallback;
}
}
ostp_server::config::ServerInbound::Api { listen, port, token, webpath, username, password_hash, .. } => {
api_config = Some(ostp_server::ApiConfig {
enabled: true,
bind: format!("{}:{}", listen, port),
token,
webpath,
username,
password_hash,
});
}
}
}
println!("{} Starting server on {:?}", "[ostp]".cyan().bold(), listen_addrs);
let debug = server_cfg.debug.unwrap_or(false);
let mut outbound = None;
for ob in server_cfg.outbounds {
if let ostp_server::config::ServerOutbound::Socks { server, port, tag } = ob {
let mut rules = Vec::new();
let mut default_action = Some("proxy".to_string());
if let Some(routing) = &server_cfg.routing {
for rule in &routing.rules {
if rule.outbound == tag {
rules.push(ostp_server::OutboundRule {
domain_suffix: rule.domain_suffix.clone(),
ip_cidr: rule.ip_cidr.clone(),
protocol: rule.protocol.clone(),
action: Some("proxy".to_string()),
});
}
}
if routing.default_outbound != tag {
default_action = Some("direct".to_string());
}
}
outbound = Some(ostp_server::OutboundConfig {
enabled: true,
protocol: "socks5".to_string(),
address: server,
port,
rules,
default_action,
});
break;
}
}
let dns_cfg = server_cfg.dns;
let host = if host_port.0 == "0.0.0.0" {
detect_local_public_ip().unwrap_or_else(|| "127.0.0.1".to_string())
} else {
host_port.0.to_string()
};
ostp_server::run_server(listen_addrs, Some(host), access_keys_meta, outbound, api_config, fallback_config, debug, dns_cfg, Some(args.config), server_cfg.license_key.clone()).await?;
}'''
content = content.replace(old_run_app_block, new_run_app_block)
with open('ostp/src/main.rs', 'w', encoding='utf-8') as f:
f.write(content)

53
test_client.json Normal file
View File

@ -0,0 +1,53 @@
{
// OSTP Configuration v0.3.1
// DO NOT EDIT THIS COMMENT - Migrator relies on it
"version": "0.3.1",
"mode": "client",
"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": "YOUR_SERVER_IP",
"port": 50000,
"access_key": "170756347f1562a4b260f8f4b419009a",
"transport": {
"type": "udp"
}
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"routing": {
"rules": [
{
"domain_suffix": ["localhost"],
"outbound": "direct"
}
],
"default_outbound": "proxy"
}
}

42
test_server.json Normal file
View File

@ -0,0 +1,42 @@
{
// OSTP Configuration v0.3.1
// DO NOT EDIT THIS COMMENT - Migrator relies on it
"version": "0.3.1",
"mode": "server",
"log": {
"level": "info"
},
// The address and port the server listens on for incoming OSTP connections.
"listen": "0.0.0.0:50000",
// List of valid keys. Clients must use one of these to connect.
"access_keys": [
"1369293f64ed6382d96cd2c1fa2ee4ee"
],
// Optional proxy for outbound traffic.
"outbound": {
"enabled": false,
"protocol": "socks5",
"address": "127.0.0.1",
"port": 9050,
"default_action": "proxy",
"rules": [
{
"domain_suffix": [".onion"],
"action": "proxy"
}
]
},
// Fallback TCP proxy: unrecognized connections are proxied to a web server (anti-DPI).
"fallback": {
"enabled": false,
"listen": "0.0.0.0:443",
// Target web server (e.g., local nginx or caddy)
"target": "127.0.0.1:8080"
},
"debug": false
}