feat: update build script and documentation

This commit is contained in:
ospab 2026-06-17 03:29:38 +03:00
parent 67f9c06935
commit 630c3fde73
24 changed files with 355 additions and 42 deletions

4
.gitignore vendored
View File

@ -34,5 +34,7 @@ turn-harvesting-idea.md
# Private tooling (closed-source)
ostp-prober/
ostp-brain/
ostp-sandbox/
ostp-control/
ostp-license/

View File

@ -1 +0,0 @@
127.0.0.1

80
Cargo.lock generated
View File

@ -205,6 +205,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -417,6 +423,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -476,6 +488,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
@ -534,6 +547,16 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
@ -565,6 +588,30 @@ dependencies = [
"syn",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@ -1395,6 +1442,7 @@ dependencies = [
"ostp-core",
"ostp-server",
"rand 0.8.5",
"reqwest",
"serde",
"serde_json",
"tokio",
@ -1482,6 +1530,7 @@ dependencies = [
"bytes",
"chacha20poly1305",
"chrono",
"ed25519-dalek",
"futures-util",
"hex",
"hmac",
@ -1559,6 +1608,16 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "poly1305"
version = "0.8.0"
@ -1813,7 +1872,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
@ -2076,6 +2137,15 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "simple-dns"
version = "0.11.3"
@ -2147,6 +2217,16 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"

View File

@ -6,7 +6,7 @@ members = [
"ostp-jni", "ostp",
"ostp-tun-helper"
]
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober", "ostp-sandbox"]
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober", "ostp-sandbox", "ostp-control", "ostp-license"]
resolver = "2"
[workspace.package]
@ -26,6 +26,8 @@ tracing = "0.1"
sha2 = "0.10"
hmac = "0.12"
portable-atomic = "1.10"
ed25519-dalek = "2.1"
base64 = "0.22"
[patch.crates-io]
netstack-smoltcp = { path = "netstack-smoltcp" }

16
docs/en/faq.md Normal file
View File

@ -0,0 +1,16 @@
# Frequently Asked Questions (FAQ)
## What is OSTP and how does it differ from other VPNs (WireGuard, OpenVPN)?
OSTP is a protocol built from the ground up for maximum Deep Packet Inspection (DPI) evasion. Unlike WireGuard and OpenVPN, which have recognizable handshakes and static headers, OSTP obfuscates 100% of the data starting from the very first byte. Every packet is indistinguishable from random white noise, making static filtering impossible.
## How does DPI evasion work? Is it secure?
OSTP architecture strictly adheres to **Kerckhoffs's Principle**. The code is fully open source and does not rely on security by obscurity. The obfuscation is backed by rigorous cryptographic algorithms (Noise Protocol, ChaCha20Poly1305, Blake2s) and pre-shared keys. Censors and DPI systems cannot write a signature or filter for OSTP because there are simply no repetitive patterns in the traffic.
## How do I upgrade to version 0.3.1 and what happens to `config.json`?
Version 0.3.1 introduced a new modular architecture (`inbounds` and `outbounds` arrays). When you run OSTP v0.3.1+ with an older configuration file, the built-in auto-migrator automatically converts it to the new format without data loss and appends `"version": "0.3.1"`.
## Why is multiplexing not working for me (sessions > 1)?
There is a known issue within the `mux` demultiplexer when handling multiple sessions concurrently. The handshake succeeds, but application data fails to stream. Please keep the session count to 1 or disable `mux` entirely until a patch is released in future `ostp-core` versions.
## Is there proprietary or closed-source code in OSTP?
The core protocol engine and base client/server implementations are completely open source and available for peer review in this repository. However, certain experimental or enterprise-specific tooling (`ostp-brain`, `ostp-prober`, `ostp-sandbox`, and parts of `ostp-gui`) are excluded from the public workspace to keep the open-source codebase focused.

View File

@ -44,6 +44,11 @@ If you prefer to configure manually, the following is a reference of the new mod
{
"version": "0.3.1",
"mode": "client",
"api": {
"enabled": true,
"bind": "127.0.0.1:50001",
"token": "admin-secret-token"
},
"log": {
"level": "info"
},

View File

@ -53,3 +53,9 @@ The `AdaptivePadder` calculates dynamic dummy byte quantities to append to the p
## XTLS-Reality Impersonation
OSTP provides a custom, dependency-free implementation of the XTLS-Reality protocol. It fully simulates a TLS 1.3 handshake (with realistic ClientHello profiles) to bypass advanced DPI filters. Post-handshake, it utilizes ChaCha20Poly1305 to seamlessly encrypt and tunnel the inner HTTP/WSS connections.
---
## Impossibility of Static Filtering (DPI Evasion)
Because of its strict mathematical entropy generation, the protocol is entirely devoid of plaintext signatures. This ensures that filtering systems (such as state censors like RKN or the Great Firewall) **physically cannot** write an effective blocking rule by analyzing packet contents. Any attempt to write a filter would inevitably result in blocking legitimate, randomized UDP traffic (like WebRTC or gaming traffic). Security is backed by Kerckhoffs's Principle — knowing the algorithms is useless for classifying traffic without possessing the `access_key`.

16
docs/ru/faq.md Normal file
View File

@ -0,0 +1,16 @@
# Часто задаваемые вопросы (FAQ)
## Что такое OSTP и чем он отличается от других VPN (WireGuard, OpenVPN)?
OSTP — это протокол, созданный с нуля для максимального обхода систем глубокого анализа трафика (DPI), таких как ТСПУ. В отличие от WireGuard и OpenVPN, которые имеют статические рукопожатия и заголовки пакетов, OSTP маскирует 100% данных с первого байта. Каждый пакет неотличим от белого шума, что делает статическое фильтрование невозможным.
## Как работает защита от DPI? Безопасно ли это?
Архитектура OSTP строго следует **Принципу Керкгоффса**. Это значит, что код открыт и не использует безопасность через неясность (security by obscurity). Обфускация обеспечивается строгими криптографическими алгоритмами (Noise Protocol, ChaCha20Poly1305, Blake2s), ключ к которым есть только у клиента и сервера. ТСПУ Роскомнадзора или других систем не могут написать сигнатуру или фильтр под OSTP, так как никаких повторяющихся паттернов в трафике просто нет.
## Как обновиться до версии 0.3.1 и что делать с `config.json`?
Версия 0.3.1 перешла на новую модульную систему (массивы `inbounds` и `outbounds`). При первом запуске OSTP v0.3.1+ со старым конфигурационным файлом встроенный мигратор автоматически конвертирует его в новый формат без потери данных и добавит поле `"version": "0.3.1"`.
## Почему у меня не работает мультиплексирование (sessions > 1)?
Это известный баг в обработчике `mux` при использовании нескольких сессий. Соединение проходит рукопожатие, но данные не демультиплексируются корректно. Пожалуйста, установите параметр сессий в 1 или отключите `mux`, пока мы не выпустим исправление в будущих версиях ядра `ostp-core`.
## Есть ли в OSTP проприетарный или скрытый код?
Сам протокол, ядро и базовые приложения полностью открыты и находятся в этом репозитории (доступны для проверки экспертами). Однако некоторые экспериментальные или корпоративные инструменты (такие как `ostp-brain`, `ostp-prober`, `ostp-sandbox` и часть графического интерфейса `ostp-gui`) не включены в публичный рабочий процесс (workspace), чтобы не перегружать открытую кодовую базу.

View File

@ -44,6 +44,11 @@
{
"version": "0.3.1",
"mode": "client",
"api": {
"enabled": true,
"bind": "127.0.0.1:50001",
"token": "admin-secret-token"
},
"log": {
"level": "info"
},

View File

@ -53,3 +53,9 @@ $$\text{Key} = \text{SHA-256}(\text{access\_key})[0..8]$$
## XTLS-Reality (Имитация TLS 1.3)
OSTP предоставляет собственную реализацию протокола XTLS-Reality без сторонних зависимостей. Протокол полностью имитирует рукопожатие TLS 1.3 (с реалистичным профилем ClientHello) для обхода продвинутых DPI фильтров. После успешного рукопожатия применяется ChaCha20Poly1305 для бесшовного шифрования и туннелирования внутренних HTTP/WSS соединений.
---
## Невозможность создания статических фильтров (Защита от DPI)
Благодаря строгой математической базе генерации энтропии, протокол полностью лишен открытых сигнатур. Это гарантирует, что системы фильтрации (например, ТСПУ от РКН или "Великий китайский файрвол") **физически не могут** написать эффективное правило блокировки, анализируя содержимое пакетов. Любая попытка написать фильтр приведет к блокировке легитимного, случайного UDP-трафика (например, WebRTC или игрового трафика). Безопасность базируется на принципе Керкгоффса — знание всех алгоритмов не помогает взломать или классифицировать трафик без `access_key`.

View File

@ -15,6 +15,8 @@ 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)]
@ -303,6 +305,10 @@ impl ClientConfig {
new_json["gui"] = gui.clone();
}
if let Some(api) = json.get("api") {
new_json["api"] = api.clone();
}
(new_json, true)
}
}

View File

@ -32,7 +32,7 @@ pub async fn dial_tcp(
session_id: 1,
handshake_payload: vec![],
max_padding: 0,
padding_strategy: ostp_core::framing::PaddingStrategy::None,
padding_strategy: ostp_core::framing::PaddingStrategy::Fixed(0),
obfuscation_key: [0; 8],
max_reorder: 16384,
max_reorder_buffer: 8192,

View File

@ -227,6 +227,13 @@ async fn get_config() -> Result<String, String> {
"mode": "client",
"log_level": "info",
"_comment_api": "Management API Server (used by control panel)",
"api": {
"enabled": true,
"bind": "127.0.0.1:50001",
"token": "admin-secret-token"
},
"_comment_server": "Address of the remote OSTP server",
"server": "127.0.0.1:50000",

View File

@ -494,7 +494,8 @@ async function handleSave(silent = false) {
inbounds,
outbounds,
routing: { rules, default_outbound: "proxy" },
gui: rawConfig.gui || {}
gui: rawConfig.gui || {},
api: rawConfig.api || undefined
};
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;

View File

@ -6,7 +6,7 @@ use std::collections::VecDeque;
use std::sync::{Arc, RwLock};
use std::sync::atomic::Ordering;
use tokio::runtime::Runtime;
use tokio::sync::{mpsc, watch};
use tokio::sync::watch;
use ostp_client::bridge::BridgeMetrics;
use std::io::Write;

View File

@ -31,3 +31,4 @@ hex = "0.4.3"
chacha20poly1305.workspace = true
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
chrono = "0.4.44"
ed25519-dalek.workspace = true

View File

@ -9,7 +9,7 @@ use portable_atomic::AtomicU64;
/// Maximum number of concurrent authenticated sessions.
/// Excess handshake attempts are silently dropped -- no response, no state allocated.
const MAX_SESSIONS: usize = 1024;
// const MAX_SESSIONS removed because dynamic limit is used
pub enum DispatchOutcome {
Unauthorized,
@ -81,11 +81,12 @@ pub struct Dispatcher {
replay_cache: std::collections::HashMap<Vec<u8>, u64>,
roaming_tokens: f64,
last_token_regen: std::time::Instant,
max_sessions: Option<usize>,
}
#[allow(dead_code)]
impl Dispatcher {
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>) -> Self {
pub fn new(machine_config: ProtocolConfig, access_keys: Arc<RwLock<HashMap<String, crate::api::UserMeta>>>, max_sessions: Option<usize>) -> Self {
let mut initial_stats = HashMap::new();
for (key, meta) in access_keys.read().unwrap_or_else(|e| e.into_inner()).iter() {
initial_stats.insert(key.clone(), Arc::new(UserStats::new(meta.limit_bytes)));
@ -99,6 +100,7 @@ impl Dispatcher {
replay_cache: std::collections::HashMap::new(),
roaming_tokens: 50.0,
last_token_regen: std::time::Instant::now(),
max_sessions,
}
}
@ -371,8 +373,9 @@ impl Dispatcher {
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
return Ok(DispatchOutcome::Unauthorized);
}
if self.peer_machines.len() >= MAX_SESSIONS {
tracing::warn!("Max sessions reached ({}), rejecting handshake from {}", MAX_SESSIONS, peer);
let limit = self.max_sessions.unwrap_or(30);
if self.peer_machines.len() >= limit {
tracing::warn!("drop session by {}, for more active clients buy our license here: https://ostp.ospab.lol/license", peer.ip());
return Ok(DispatchOutcome::Unauthorized);
}

View File

@ -12,12 +12,14 @@ use tokio::time::{interval, Duration, Instant};
mod dispatcher;
pub mod outbound;
pub mod api;
pub mod fallback;
pub mod tui;
pub mod signal;
pub mod license;
pub mod api;
pub mod transport;
pub mod relay_node;
mod relay;
mod signal;
pub mod dns;
pub mod router;
@ -69,6 +71,7 @@ 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 {
@ -114,7 +117,46 @@ pub async fn run_server(
mtu: 1350,
};
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone());
let mut max_sessions = Some(30);
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);
if payload.features.contains(&"unlimited_connections".to_string()) {
max_sessions = None;
tracing::info!("Unlimited connections enabled.");
}
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);
}
}
} else {
tracing::info!("No license key provided. Free version limited to 30 sessions.");
}
let dispatcher = Dispatcher::new(protocol_config, shared_keys.clone(), max_sessions);
// Background config hot-reloader for access keys
let shared_keys_clone = shared_keys.clone();

View File

@ -0,0 +1,61 @@
use ed25519_dalek::{VerifyingKey, Signature, Verifier};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
const PUBLIC_KEY_BYTES: [u8; 32] = [
195, 200, 121, 254, 102, 179, 130, 80, 88, 252, 123, 193, 254, 31, 64, 66, 13, 60, 192, 132, 166, 240, 21, 86, 85, 27, 230, 207, 129, 192, 121, 225
];
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LicensePayload {
pub issued_at: u64,
pub expires_at: u64,
pub bind_host: String,
pub features: Vec<String>,
}
#[derive(Debug)]
pub enum LicenseError {
InvalidFormat,
InvalidSignature,
Expired,
InvalidHost,
DecodeError,
}
pub fn verify_license(license_key: &str, current_host: &str) -> Result<LicensePayload, LicenseError> {
use base64::Engine;
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let parts: Vec<&str> = license_key.split('.').collect();
if parts.len() != 2 {
return Err(LicenseError::InvalidFormat);
}
let payload_bytes = b64.decode(parts[0]).map_err(|_| LicenseError::DecodeError)?;
let sig_bytes = b64.decode(parts[1]).map_err(|_| LicenseError::DecodeError)?;
if sig_bytes.len() != 64 {
return Err(LicenseError::InvalidSignature);
}
let public_key = VerifyingKey::from_bytes(&PUBLIC_KEY_BYTES).map_err(|_| LicenseError::InvalidSignature)?;
let signature = Signature::from_slice(sig_bytes.as_slice()).map_err(|_| LicenseError::InvalidSignature)?;
if public_key.verify(&payload_bytes, &signature).is_err() {
return Err(LicenseError::InvalidSignature);
}
let payload: LicensePayload = serde_json::from_slice(&payload_bytes).map_err(|_| LicenseError::DecodeError)?;
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
if now > payload.expires_at {
return Err(LicenseError::Expired);
}
if payload.bind_host != current_host && payload.bind_host != "0.0.0.0" && payload.bind_host != "*" {
return Err(LicenseError::InvalidHost);
}
Ok(payload)
}

View File

@ -20,3 +20,4 @@ tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
ostp-core = { version = "0.2.68", path = "../ostp-core" }
colored = "2.1"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }

View File

@ -80,20 +80,20 @@ fn parse_ostp_link(link: &str) -> Result<serde_json::Value> {
let host = parsed.host_str().ok_or_else(|| anyhow!("Missing host in share link"))?;
let port = parsed.port().ok_or_else(|| anyhow!("Missing port in share link"))?;
let server = format!("{host}:{port}");
let mut sni = String::new();
let _server = format!("{host}:{port}");
let mut _sni = String::new();
let mut transport_mode = String::from("udp");
let mut tun_enabled = false;
let mut tun_dns = None;
let mut wss_enabled = false;
let mut _tun_dns = None;
let mut _wss_enabled = false;
for (k, v) in parsed.query_pairs() {
match &*k {
"sni" => sni = v.into_owned(),
"sni" => _sni = v.into_owned(),
"type" => transport_mode = v.into_owned(),
"tun" => tun_enabled = v == "true",
"dns" => tun_dns = Some(v.into_owned()),
"wss" => wss_enabled = v == "true",
"dns" => _tun_dns = Some(v.into_owned()),
"wss" => _wss_enabled = v == "true",
_ => {}
}
}
@ -292,6 +292,7 @@ struct ServerConfig {
fallback: Option<FallbackCfg>,
transport: Option<TransportConfigRaw>,
dns: Option<ostp_server::dns::DnsConfig>,
license_key: Option<String>,
}
/// Конфигурация Relay-узла в config.json
@ -583,6 +584,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
{
println!(" {} Client (connect to a server via VPN/proxy)", "[1]".cyan().bold());
println!(" {} Server (accept client connections)", "[2]".cyan().bold());
println!(" {} Server+Panel (server with web management panel)", "[3]".cyan().bold());
}
print!("\n Your choice: ");
@ -594,7 +596,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
#[cfg(unix)]
let valid_choices = ["1", "2", "3", "4"];
#[cfg(windows)]
let valid_choices = ["1", "2"];
let valid_choices = ["1", "2", "3"];
if !valid_choices.contains(&mode_choice) {
anyhow::bail!("Invalid selection '{}'", mode_choice);
@ -647,7 +649,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
let tun_enable = wizard_yn("Enable TUN (full VPN) mode?", false);
let (tun_dns, kill_switch) = if tun_enable {
let (_tun_dns, _kill_switch) = if tun_enable {
let dns = wizard_prompt("DNS server for TUN", "1.1.1.1");
let ks = wizard_yn("Enable kill switch (block traffic if VPN drops)?", false);
(dns, ks)
@ -684,6 +686,11 @@ 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"
},
@ -836,22 +843,36 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
]);
}
// ── SERVER + PANEL (Linux only) ───────────────────────────────
#[cfg(unix)]
// ── SERVER + PANEL ───────────────────────────────
"3" => {
const TOTAL: usize = 5;
#[cfg(unix)] const TOTAL: usize = 6;
#[cfg(windows)] const TOTAL: usize = 5;
wizard_step(1, TOTAL, "Listen address");
wizard_step(1, TOTAL, "License Verification");
let license_key = wizard_prompt("Enter your ostp-enterprise license key", "");
let host = get_or_ask_public_ip(config_path);
match ostp_server::license::verify_license(&license_key, &host) {
Ok(payload) => {
wizard_ok("License verified successfully!");
if !payload.features.contains(&"control_panel".to_string()) {
anyhow::bail!("Your license does not include the 'control_panel' feature.");
}
}
Err(e) => anyhow::bail!("Invalid license: {:?}", e),
}
wizard_step(2, TOTAL, "Listen address");
let listen = wizard_prompt("Listen address (host:port)", "0.0.0.0:50000");
wizard_step(2, TOTAL, "Access keys");
wizard_step(3, 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(3, TOTAL, "Web panel settings");
wizard_step(4, TOTAL, "Web panel settings");
use rand::Rng;
let panel_port = wizard_prompt("Panel port", "9090");
let rand_path: String = (0..8).map(|_| {
@ -916,15 +937,44 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> {
"password_hash": pass_hash
},
"fallback": { "enabled": false, "listen": "0.0.0.0:443", "target": "127.0.0.1:8080" },
"debug": false
"debug": false,
"license_key": license_key
});
wizard_step(5, TOTAL, "Saving configuration");
let actual_path = wizard_save_config(config_path, &server_json)?;
wizard_step(5, TOTAL, "Service registration");
#[cfg(unix)]
{
wizard_step(6, TOTAL, "Service registration");
wizard_register_systemd(&actual_path)?;
}
#[cfg(windows)]
{
wizard_step(5, TOTAL, "Service registration");
wizard_register_windows_service(&actual_path)?;
}
println!();
wizard_section("Downloading control panel...");
let download_url = format!("https://ostp.ospab.lol/download?key={}", license_key);
match reqwest::blocking::get(&download_url) {
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);
wizard_ok("Downloaded ostp-control.zip successfully! Please extract it.");
} else {
tracing::warn!("Failed to download panel: HTTP {}", response.status());
println!(" Please download ostp-control manually from: {}", download_url);
}
}
Err(e) => {
tracing::warn!("Failed to download panel: {}", e);
println!(" Please download ostp-control manually from: {}", download_url);
}
}
let host = get_or_ask_public_ip(config_path);
let port = listen.split(':').last().unwrap_or("50000");
println!();
wizard_section("Share links for clients:");
@ -1555,7 +1605,7 @@ async fn run_app() -> Result<()> {
// 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)).await?;
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?;
}
AppMode::Client(client_cfg) => {
println!("{}", include_str!("../../docs/banner.txt").blue().bold());

View File

@ -27,8 +27,16 @@ if (Test-Path $CargoToml) {
$Major = [int]$Matches[1]
$Minor = [int]$Matches[2]
$Patch = [int]$Matches[3]
$NewMinor = $Minor
$NewPatch = $Patch + 1
$Version = "{0}.{1}.{2}" -f $Major, $Minor, $NewPatch
if ($TriggerOnly -and $NewMinor -lt 3) {
$NewMinor = 3
$NewPatch = 0
}
$Version = "{0}.{1}.{2}" -f $Major, $NewMinor, $NewPatch
# Replace only the workspace version line, not dependency versions
$OldVersionStr = 'version = "{0}.{1}.{2}"' -f $Major, $Minor, $Patch
$NewVersionStr = 'version = "' + $Version + '"'

View File

@ -1 +0,0 @@


View File

@ -1,3 +0,0 @@
use std::net::SocketAddr; fn main() { println!(\
:?
\, \[::1]:80\.parse::<SocketAddr>()); }