mirror of https://github.com/ospab/ostp.git
feat: make panel open source, remove license check, and restore rust-embed
This commit is contained in:
parent
9e2ab59121
commit
5782107c84
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
218
README.md
218
README.md
|
|
@ -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)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> 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
|
||||
flowchart LR
|
||||
Apps[Local Apps] -->|SOCKS5 / TUN| CoreC
|
||||
|
||||
subgraph OSTPCoreClient ["OSTP Core Protocol"]
|
||||
B --> C{Protocol Machine}
|
||||
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
|
||||
D -->|Obfuscated UDP Payload| E((UDP Socket))
|
||||
end
|
||||
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
|
||||
|
|
|
|||
166
README.ru.md
166
README.ru.md
|
|
@ -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)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> Быстрый кастомный зашифрованный транспортный протокол на 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
|
||||
flowchart LR
|
||||
Apps[Приложения] -->|SOCKS5 / TUN| CoreC
|
||||
|
||||
subgraph OSTPCoreClient ["OSTP Core Протокол"]
|
||||
B --> C{Protocol Machine}
|
||||
C -->|Noise Handshake| D[ChaCha20Poly1305 AEAD]
|
||||
D -->|Обфусцированный UDP| E((UDP Сокет))
|
||||
end
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ostp-gui"
|
||||
version = "0.3.3"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
407
ostp/src/main.rs
407
ostp/src/main.rs
|
|
@ -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", "");
|
||||
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);
|
||||
|
||||
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");
|
||||
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
|
||||
|
||||
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 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 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;
|
||||
// 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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue