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-brain/
ostp-sandbox/
ostp-control/
ostp-license/
ostp-web/
# Web panel
ostp-control/node_modules/

35
Cargo.lock generated
View File

@ -1540,6 +1540,7 @@ dependencies = [
"portable-atomic",
"rand 0.8.5",
"reqwest",
"rust-embed",
"serde",
"serde_json",
"sha2",
@ -1917,6 +1918,40 @@ dependencies = [
"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]]
name = "rustc-hash"
version = "2.1.2"

220
README.md
View File

@ -1,131 +1,101 @@
# 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)
![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)
![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)
> A fast, custom encrypted transport protocol written in Rust.
**OSTP** (Ospab Stealth Transport Protocol) is a high-performance transport protocol. It implements a custom ARQ transport over UDP, as well as a UoT (UDP-over-TCP) mode. Every byte on the wire — including packet headers — is cryptographically indistinguishable from random noise, making it highly resistant to Deep Packet Inspection (DPI).
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.
> [!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
```bash
bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)
```
### 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. |
| Capability | Description |
|------------|-------------|
| **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. |
| **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. |
| **Seamless Roaming** | Clients can switch networks (WiFi ↔ LTE) without session interruption — tracked by session-ID, not IP. |
| **Management API** | Built-in REST API for third-party panels (3x-ui, custom dashboards). Per-user stats, traffic limits, key CRUD. |
| **Fallback Server** | TCP fallback proxy to a web server — makes OSTP indistinguishable from nginx during active probing. |
| **Multi-Listener** | Bind to multiple addresses simultaneously (dual-stack IPv4/IPv6, multi-port). |
| **TUN Mode** | Full-system VPN via native `smoltcp` network stack without external dependencies. All traffic transparently routed through the tunnel. |
| **xHTTP Stealth (UoT)** | UDP-over-TCP tunnel that completely hides traffic. Since all data is fully encrypted and length-prefixed, it bypasses DPI filters that block unknown UDP traffic by riding over a plain TCP connection. |
| **Mobile Apps** | Beautiful cross-platform mobile client (Flutter) for effortless client management. |
| **TURN Relay** | RFC 5766 TURN support for environments where direct UDP is blocked. |
| **Hot-Reload** | Runtime config reload without restart (access keys, exclusions, mux settings). |
| **Structured Logging** | `tracing`-based logging with `RUST_LOG` filtering. JSON/file/syslog output support. |
| **Cross-Platform** | Windows, Linux, macOS, Android, FreeBSD, MIPS, RISC-V. Single binary, no runtime dependencies. |
| **Multiplexed Streams**| Multiple logical TCP streams over a single encrypted UDP session with per-stream flow control. |
| **Session Roaming** | Connection persistence across IP changes via session ID tracking. |
| **UoT Mode** | UDP-over-TCP encapsulation with length-prefixing to bypass UDP blocking. |
| **Fallback Server** | TCP proxying to a legitimate web server to resist active probing. |
| **TUN Mode** | Native network stack integration (`smoltcp`) for full-system routing without external dependencies. |
| **Management API** | Built-in REST API for server administration, metrics, and key generation. |
| **TURN Relay** | RFC 5766 TURN support for NAT traversal. |
---
## Architecture
```mermaid
graph TD
subgraph Client ["Client"]
A[Browser / Apps] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
TUN[TUN Interface] -->|IP Packets| B
subgraph OSTPCoreClient ["OSTP Core Protocol"]
B --> C{Protocol Machine}
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Obfuscated UDP Payload| E((UDP Socket))
end
flowchart LR
Apps[Local Apps] -->|SOCKS5 / TUN| CoreC
subgraph Client [Client Node]
CoreC[OSTP Client] -.->|Encrypt & Mask| NetC[Transport Layer]
end
E <==>|Encrypted & Obfuscated UDP Tunnel| F
NetC <==>|Encrypted UDP / UoT| NetS
subgraph Server ["Server"]
F((UDP Socket)) --> G{Dispatcher}
subgraph OSTPCoreServer ["OSTP Core Backend"]
G -->|Auth & Decrypt| H[Session & State Guard]
H -->|TCP Stream| I[Relay Loop]
end
G -->|Active Probing / Unauth| FB[TCP Fallback Proxy]
FB -->|Forward| NGINX[nginx / Caddy]
H -->|Stats & Traffic| API[Management API]
I -->|Outbound| WWW((Internet))
subgraph Server [Server Node]
NetS[Transport Layer] -.->|Decrypt & Auth| CoreS[OSTP Server]
NetS -->|Unauthenticated| Fallback[Fallback Server]
end
CoreS -->|Relay| WWW((Internet))
Fallback -->|Forward| Web((Web / NGINX))
```
---
## Quick Start
### 1. Generate config
### 1. Installation
**Linux:**
```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
# On your machine (client):
# On the client:
./ostp --init client
```
### 2. Edit config
**Server** — set your access keys:
**Server Example** (`config.json`):
```jsonc
{
"mode": "server",
"listen": "0.0.0.0:50000",
"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" }
"access_keys": ["YOUR_SECRET_KEY"]
}
```
**Client** — point to your server:
**Client Example** (`config.json`):
```jsonc
{
"mode": "client",
"version": "0.3.1",
"log": { "level": "info" },
"inbounds": [
{ "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 }
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 }
],
"outbounds": [
{
@ -135,90 +105,35 @@ graph TD
"port": 50000,
"access_key": "YOUR_SECRET_KEY",
"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. Run
### 3. Execution
```bash
./ostp # Uses config.json in current directory
./ostp --config /path/to.json # Custom config path
./ostp --check # Validate config without running
./ostp --generate-key # Generate a new access key
./ostp --links # Print client share links
# Run with default config.json
./ostp
# Run with a specific config path
./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
./ostp "ostp://ACCESS_KEY@server.com:50000?..."
```
> [!WARNING]
> Always wrap the `ostp://...` link in quotes (`"`) so your terminal doesn't misinterpret special characters like `&` or `?`.
---
## Management API
Built-in REST API for building panels and dashboards.
```bash
# Server status
curl -H "Authorization: Bearer mytoken" http://127.0.0.1:9090/api/server/status
# List all users with traffic stats
curl -H "Authorization: Bearer mytoken" http://127.0.0.1:9090/api/users
# Create a user with 10GB traffic limit
curl -X POST -H "Authorization: Bearer mytoken" \
-H "Content-Type: application/json" \
-d '{"limit_bytes": 10737418240}' \
http://127.0.0.1:9090/api/users
```
Full API reference: [Management API](https://github.com/ospab/ostp/wiki/Management-API)
---
## CLI Reference
```
ostp [OPTIONS] [URL]
Options:
--config <PATH> Config file path (default: config.json)
--init <MODE> Generate template config (server/client)
--check Validate configuration and exit
-g, --generate-key Generate a secure access key
-c, --count <N> Number of keys to generate (default: 1)
--format <FMT> Key format: hex, base64 (default: hex)
--links Print client share links from server config
Arguments:
[URL] Connect via share link: ostp://KEY@HOST:PORT
./ostp "ostp://YOUR_SECRET_KEY@YOUR_SERVER_IP:50000?transport=udp"
```
---
## Protocol Summary
## Protocol Specification
| Layer | Mechanism |
|-------|-----------|
| Key Exchange | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT |
| 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 |
| Retransmission | Rate-limited NACK + exponential backoff RTO |
| Keepalive | Ping/Pong with RTT measurement every 5s |
@ -228,38 +143,31 @@ Arguments:
## Building from Source
```bash
# Prerequisites: Rust 1.75+
# Requires Rust 1.75+
cargo build --release
# Cross-compile for Linux
cross build --release --target x86_64-unknown-linux-gnu
# Run tests
cargo test -p ostp-core -p ostp-server
```
---
## Documentation
- **[Wiki](https://github.com/ospab/ostp/wiki)** — Full documentation
- [Installation](https://github.com/ospab/ostp/wiki/Installation)
- **[Wiki](https://github.com/ospab/ostp/wiki)**
- [Configuration Reference](https://github.com/ospab/ostp/wiki/Configuration)
- [Management API](https://github.com/ospab/ostp/wiki/Management-API)
- [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
Business Source License 1.1. Free for personal and non-commercial use.
Converts to MIT License on May 14, 2030.
GNU Affero General Public License v3.0 (AGPL-3.0). See [LICENSE](LICENSE) for more details.
---
## Contact
## Contacts
- **Telegram**: [@ospab0](https://t.me/ospab0)
- **Email**: gvoprgrg@gmail.com

View File

@ -1,123 +1,101 @@
# 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)
![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)
![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)
> Быстрый кастомный зашифрованный транспортный протокол на Rust.
**OSTP** (Ospab Stealth Transport Protocol) — кастомный транспортный протокол. Реализует собственный ARQ-транспорт поверх UDP, а также режим UoT (UDP-over-TCP). Каждый байт, включая заголовки пакетов, криптографически неотличим от случайного шума, что делает его устойчивым к системам глубокого анализа трафика (DPI).
OSTP (Ospab Stealth Transport Protocol) — зашифрованный транспортный протокол, написанный на Rust. Реализует механизм ARQ поверх UDP, а также режим UoT (UDP-over-TCP). Протокол использует криптографическое маскирование заголовков и полезной нагрузки для защиты от систем глубокого анализа трафика (DPI).
> [!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-ключами, уникальными для каждого пакета. |
| **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — аутентификация через PSK, forward secrecy, без раскрытия идентичности. |
| **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. Разработан для 10 Гбит/с. |
| **Маскирование трафика** | Шифрование заголовков и полезной нагрузки с помощью HMAC ключей на каждый пакет. Трафик неотличим от шума. |
| **Noise Protocol** | `Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s` — аутентификация через PSK, forward secrecy. |
| **Reliable UDP (ARQ)** | Selective ACK/NACK с rate-limited ретрансмиссией, настраиваемым reorder-буфером и exponential backoff. |
| **Мультиплексирование** | Несколько логических TCP-потоков поверх одной зашифрованной UDP-сессии с per-stream flow control. |
| **Бесшовный роуминг** | Клиент может менять сети (WiFi ↔ 4G) без разрыва сессии — сервер отслеживает session-ID, а не IP-адрес. |
| **TUN-режим** | Полносистемный VPN без внешних зависимостей (встроенный network stack на базе `smoltcp`). |
| **xHTTP Стелс (UoT)** | Туннель UDP-over-TCP, который полностью скрывает трафик. Поскольку все данные полностью зашифрованы и имеют префикс длины, он обходит DPI фильтры, блокирующие неизвестный UDP трафик, передавая всё по обычному TCP соединению. |
| **Мобильные приложения** | Красивый кроссплатформенный мобильный клиент (Flutter) для удобного администрирования. |
| **TURN Relay** | RFC 5766 TURN для окружений, где прямой UDP заблокирован. |
| **Hot-Reload** | Перезагрузка конфига в рантайме без перезапуска (ключи, исключения, mux, TURN). |
| **Кросс-платформа** | Windows, Linux, macOS, Android. Один бинарник, без зависимостей. |
| **Session Roaming** | Сохранение соединения при смене IP-адреса благодаря отслеживанию по идентификатору сессии (session ID). |
| **Режим UoT** | Инкапсуляция UDP внутри TCP с указанием длины пакетов для обхода блокировок неизвестного UDP-трафика. |
| **Fallback Server** | Проксирование неаутентифицированных TCP подключений на веб-сервер для защиты от активного пробинга. |
| **TUN-режим** | Полносистемная маршрутизация через встроенный сетевой стек `smoltcp` без внешних зависимостей. |
| **Management API** | Встроенный REST API для администрирования сервера, сбора метрик и генерации ключей. |
| **TURN Relay** | Поддержка RFC 5766 TURN для обхода NAT. |
---
## Архитектура
```mermaid
graph TD
subgraph Client ["Клиент"]
A[Браузер / Прил.] -->|SOCKS5 / HTTP| B(Bridge Multiplexer)
TUN[TUN Интерфейс] -->|IP Пакеты| B
subgraph OSTPCoreClient ["OSTP Core Протокол"]
B --> C{Protocol Machine}
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
D -->|Обфусцированный UDP| E((UDP Сокет))
end
flowchart LR
Apps[Приложения] -->|SOCKS5 / TUN| CoreC
subgraph Client [Клиент]
CoreC[OSTP Клиент] -.->|Шифрование| NetC[Транспортный уровень]
end
E <==>|Зашифрованный UDP Туннель| F
NetC <==>|Зашифрованный UDP / UoT| NetS
subgraph Server ["Сервер"]
F((UDP Сокет)) --> G{Dispatcher}
subgraph OSTPCoreServer ["OSTP Core Backend"]
G -->|Auth & Decrypt| H[Session & State Guard]
H -->|TCP Поток| I[Relay Loop]
end
G -->|Active Probing / Unauth| FB[TCP Fallback Proxy]
FB -->|Перенаправление| NGINX[nginx / Caddy]
I -->|Outbound| WWW((Интернет))
subgraph Server [Сервер]
NetS[Транспортный уровень] -.->|Дешифрование| CoreS[OSTP Сервер]
NetS -->|Неавторизованные| Fallback[Fallback Сервер]
end
CoreS -->|Проксирование| WWW((Интернет))
Fallback -->|Перенаправление| Web((Веб-сервер / NGINX))
```
---
## Установка
## Быстрый старт
### Linux
### 1. Установка
**Linux:**
```bash
bash <(curl -Ls https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.sh)
```
### Windows (PowerShell от Администратора)
**Windows (PowerShell от Администратора):**
```powershell
irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | iex
```
---
### 2. Конфигурация
## Конфигурация
Создать конфиг по умолчанию:
Сгенерируйте базовые файлы конфигурации:
```bash
./ostp --init server # VPS
./ostp --init client # Локальная машина
# На сервере:
./ostp --init server
# На клиенте:
./ostp --init client
```
### Сервер (`config.json`)
**Пример конфигурации сервера** (`config.json`):
```jsonc
{
"mode": "server",
"listen": "0.0.0.0:50000",
"access_keys": ["ВАШ_КЛЮЧ"],
"debug": false,
// Опционально: проксировать трафик через upstream
"outbound": {
"enabled": false,
"protocol": "socks5",
"address": "127.0.0.1",
"port": 9050,
"default_action": "proxy"
}
"access_keys": ["ВАШ_КЛЮЧ"]
}
```
### Клиент (`config.json`)
**Пример конфигурации клиента** (`config.json`):
```jsonc
{
"mode": "client",
"version": "0.3.1",
"log": { "level": "info" },
"inbounds": [
{ "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 }
{ "type": "local_proxy", "tag": "socks-in", "protocol": "socks", "listen": "127.0.0.1", "port": 1088 }
],
"outbounds": [
{
@ -126,41 +104,26 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
"server": "IP_СЕРВЕРА",
"port": 50000,
"access_key": "ВАШ_КЛЮЧ",
"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"
}
"transport": { "type": "udp" }
}
]
}
```
> **Примечание:** Обновляетесь с v0.2.x? Прочтите [Гайд по миграции на v0.3.1](MIGRATION_V0_3_1.md).
---
## Использование
### 3. Запуск
```bash
# Запуск с конфигом
./ostp --config config.json
# Или просто (ищет config.json рядом с бинарником)
# Запуск с конфигурацией по умолчанию (config.json)
./ostp
# Запуск с указанием пути к конфигурации
./ostp --config /path/to/config.json
```
### TUN-режим (Windows)
Использует встроенный сетевой стек `smoltcp` и виртуальный адаптер `wintun` (необходима `wintun.dll`). Требует запуска с правами Администратора.
### TUN-режим (Linux)
Использует встроенный сетевой стек `smoltcp` и `/dev/net/tun`. Требует запуска от имени `root` (или наличия `CAP_NET_ADMIN`).
Либо подключение через однострочную ссылку на стороне клиента:
```bash
./ostp "ostp://ВАШ_КЛЮЧ@IP_СЕРВЕРА:50000?transport=udp"
```
---
@ -170,19 +133,17 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
|---------|----------|
| Обмен ключами | Noise NNpsk0 (X25519 + ChaChaPoly + BLAKE2s) zero-RTT |
| Шифрование | ChaCha20-Poly1305 AEAD на каждый пакет |
| Обфускация заголовков | HMAC-SHA256 маска session_id + nonce, уникальная для каждого пакета |
| Обфускация заголовков | HMAC-SHA256 маска на основе session_id и nonce |
| Надёжность | Selective ACK с cumulative + SACK диапазонами |
| Ретрансмиссия | Rate-limited NACK (30мс cooldown) + exponential backoff RTO |
| Flow Control | Окно in-flight (только retransmittable фреймы) |
| Ретрансмиссия | Rate-limited NACK + exponential backoff RTO |
| Keepalive | Ping/Pong с измерением RTT каждые 5с |
| Таймаут сессии | 60с на клиенте, 300с на сервере |
---
## Сборка из исходников
```bash
# Требования: Rust toolchain (1.75+)
# Требования: Rust 1.75+
cargo build --release
# Кросс-компиляция для 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/obfuscation.md)
- [Администрирование сервера](docs/ru/server.md)
- [Архитектура](docs/ru/architecture.md)
- [Настройка клиента](docs/ru/client.md)
- [Интеграции](docs/ru/integrations.md)
---
## Лицензия
Business Source License 1.1. Бесплатно для личного и некоммерческого использования.
Переходит в MIT License 14 мая 2030 года.
GNU Affero General Public License v3.0 (AGPL-3.0). Подробнее см. в файле [LICENSE](LICENSE).
---
## Контакты
- **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,
#[serde(default, skip_serializing_if = "Option::is_none")]
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)]
@ -305,10 +303,6 @@ impl ClientConfig {
new_json["gui"] = gui.clone();
}
if let Some(api) = json.get("api") {
new_json["api"] = api.clone();
}
(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
# 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.
version: 0.3.4+17
version: 0.3.6+19
environment:
sdk: ^3.11.4

View File

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

View File

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

View File

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

View File

@ -53,7 +53,6 @@ pub struct ApiState {
pub dns_server: std::sync::Arc<crate::dns::DnsServer>,
pub audit_logs: Arc<RwLock<Vec<AuditLogEntry>>>,
pub router: std::sync::Arc<crate::router::Router>,
pub is_licensed: bool,
}
#[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()) }))
}
async fn static_handler(State(_state): State<ApiState>, _uri: Uri) -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Control panel not bundled").into_response()
#[derive(rust_embed::RustEmbed)]
#[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 ───────────────────────────────────────────────────────────────
@ -208,8 +232,7 @@ pub fn create_api_router(state: ApiState) -> Router {
.delete(handle_clear_audit),
)
.route("/users/bulk", post(handle_bulk_create_users))
.route("/router/rules", get(handle_get_rules).put(handle_put_rules))
.layer(axum::middleware::from_fn_with_state(state.clone(), license_middleware));
.route("/router/rules", get(handle_get_rules).put(handle_put_rules));
let webpath = state.webpath.clone();
let webpath = webpath.trim_matches('/');
@ -237,26 +260,6 @@ pub fn create_api_router(state: ApiState) -> Router {
.layer(cors)
.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.
pub async fn start_api_server(
config: ApiConfig,
@ -267,7 +270,6 @@ pub async fn start_api_server(
config_path: Option<std::path::PathBuf>,
dns_server: std::sync::Arc<crate::dns::DnsServer>,
router: std::sync::Arc<crate::router::Router>,
is_licensed: bool,
) {
let state = ApiState {
access_keys,
@ -284,7 +286,6 @@ pub async fn start_api_server(
dns_server,
audit_logs: Arc::new(RwLock::new(Vec::new())),
router,
is_licensed,
};
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 tui;
pub mod signal;
pub mod license;
pub mod api;
pub mod transport;
pub mod relay_node;
mod relay;
pub mod dns;
pub mod router;
pub mod config;
pub use outbound::{OutboundAction, OutboundConfig, OutboundRule};
pub use api::ApiConfig;
@ -71,7 +72,6 @@ pub async fn run_server(
debug: bool,
dns_config: Option<dns::DnsConfig>,
config_path: Option<std::path::PathBuf>,
license_key: Option<String>,
) -> Result<()> {
let mut keys_map = HashMap::new();
for (key, meta) in access_keys {
@ -117,42 +117,6 @@ pub async fn run_server(
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());
// Background config hot-reloader for access keys
@ -304,7 +268,7 @@ pub async fn run_server(
let dns_server_api = dns_server.clone();
let router_api = router.clone();
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)
- [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<()> {
match &self.mode {
AppMode::Server(cfg) => {
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 {
let action = outbound.default_action.as_deref().unwrap_or("direct");
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");
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.");
}
}
AppMode::Client(cfg) => {
if let Some(outbounds) = cfg.get("outbounds").and_then(|o| o.as_array()) {
@ -287,18 +286,7 @@ struct TransportConfigRaw {
wss: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize)]
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>,
}
type ServerConfig = ostp_server::config::ModularServerConfig;
/// Конфигурация Relay-узла в config.json
#[derive(Debug, Deserialize, Serialize)]
@ -691,11 +679,6 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
let client_json = serde_json::json!({
"mode": "client",
"version": "0.3.1",
"api": {
"enabled": true,
"bind": "127.0.0.1:50001",
"token": key_for_gen.clone()
},
"log": {
"level": "info"
},
@ -855,31 +838,18 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
#[cfg(unix)] const TOTAL: usize = 6;
#[cfg(windows)] const TOTAL: usize = 5;
wizard_step(1, TOTAL, "License Verification");
let license_key = wizard_prompt("Enter your ostp-enterprise license key", "");
let host = get_or_ask_public_ip(config_path);
match ostp_server::license::verify_license(&license_key, &host) {
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");
wizard_step(1, TOTAL, "Listen address");
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
let host = get_or_ask_public_ip(config_path);
wizard_step(3, TOTAL, "Access keys");
wizard_step(2, TOTAL, "Access keys");
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 mut access_keys: Vec<String> = Vec::new();
for _ in 0..key_count { access_keys.push(generate_secure_key("hex")); }
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;
let panel_port = wizard_prompt("Panel port", "9090");
let rand_path: String = (0..8).map(|_| {
@ -928,34 +898,38 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
"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
"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"
}
]
});
wizard_step(5, TOTAL, "Saving configuration");
wizard_step(4, TOTAL, "Saving configuration");
let actual_path = wizard_save_config(config_path, &server_json)?;
#[cfg(unix)]
{
wizard_step(6, TOTAL, "Service registration");
wizard_step(5, TOTAL, "Service registration");
wizard_register_systemd(&actual_path)?;
}
#[cfg(windows)]
@ -964,59 +938,6 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
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");
println!();
wizard_section("Share links for clients:");
@ -1351,23 +1272,33 @@ async fn run_app() -> Result<()> {
match &config.mode {
AppMode::Server(s) => {
println!("{} Config OK: server mode", "[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!(" API: {} (bind: {})",
if api.enabled.unwrap_or(false) { "enabled" } else { "disabled" },
api.bind.as_deref().unwrap_or("127.0.0.1:9090"));
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());
}
}
}
if let Some(outbound) = &s.outbound {
println!(" Outbound proxy: {} ({})",
if outbound.enabled { "enabled" } else { "disabled" },
outbound.protocol);
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(fb) = &s.fallback {
println!(" Fallback: {} ({} -> {})",
if fb.enabled.unwrap_or(false) { "enabled" } else { "disabled" },
fb.listen.as_deref().unwrap_or("0.0.0.0:443"),
fb.target.as_deref().unwrap_or("127.0.0.1:8080"));
if let Some(dns) = &s.dns {
println!(" DNS Proxy: Listen 127.0.0.1:{}", dns.local_port.to_string().cyan());
}
}
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": {{
"enabled": false,
@ -1486,11 +1408,6 @@ async fn run_app() -> Result<()> {
// DO NOT EDIT THIS COMMENT - Migrator relies on it
"version": "0.3.1",
"mode": "client",
"api": {{
"enabled": true,
"bind": "127.0.0.1:50001",
"token": "{key}"
}},
"log": {{
"level": "info"
}},
@ -1515,7 +1432,7 @@ async fn run_app() -> Result<()> {
"tag": "proxy",
"server": "YOUR_SERVER_IP",
"port": 50000,
"access_key": "YOUR_ACCESS_KEY",
"access_key": "{key}",
"transport": {{
"type": "udp"
}},
@ -1536,7 +1453,7 @@ async fn run_app() -> Result<()> {
"routing": {{
"rules": [
{{
"domain_suffix": ["localhost", "127.0.0.1"],
"domain_suffix": ["localhost"],
"outbound": "direct"
}}
],
@ -1556,18 +1473,28 @@ async fn run_app() -> Result<()> {
let mut stripped = json_comments::StripComments::new(content.as_bytes());
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
if let AppMode::Server(s) = &config.mode {
let key = &s.access_keys[0];
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://{}@{}:50000", key.key(), host);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
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 mut query_params = Vec::<String>::new();
query_params.push("type=udp".to_string());
let mut link = format!("ostp://{}@{}:50000", key, host);
if !query_params.is_empty() {
link.push('?');
link.push_str(&query_params.join("&"));
}
println!("\n Share link for client distribution:");
println!(" {}", link);
}
println!("\n Share link for client distribution:");
println!(" {}", link);
}
}
}
@ -1622,23 +1549,44 @@ async fn run_app() -> Result<()> {
config.validate()?;
if args.links {
match config.mode {
match &config.mode {
AppMode::Server(server_cfg) => {
let listen = server_cfg.listen.primary();
let parts: Vec<&str> = listen.split(':').collect();
let port = parts.get(1).unwrap_or(&"50000");
let host = if parts[0] == "0.0.0.0" {
get_or_ask_public_ip(&args.config)
} else {
parts[0].to_string()
};
let mut host = "127.0.0.1".to_string();
let mut port = 50000;
let mut users = Vec::new();
for inbound in &server_cfg.inbounds {
if let ostp_server::config::ServerInbound::Ostp { listen: l, port: p, users: u, .. } = inbound {
if l != "0.0.0.0" {
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);
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();
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() {
link.push('?');
link.push_str(&query_params.join("&"));
@ -1660,51 +1608,84 @@ async fn run_app() -> Result<()> {
AppMode::Server(server_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold());
let listen_addrs = server_cfg.listen.addresses();
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: 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 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 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 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?;
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) => {
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
}