diff --git a/.gitignore b/.gitignore index 44e9d4d..c333f85 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,7 @@ turn-harvesting-idea.md # Private tooling (closed-source) ostp-prober/ - ostp-brain/ +ostp-sandbox/ +ostp-control/ +ostp-license/ diff --git a/.ostp_public_ip b/.ostp_public_ip deleted file mode 100644 index e56ea71..0000000 --- a/.ostp_public_ip +++ /dev/null @@ -1 +0,0 @@ -127.0.0.1 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6e4bae9..6777eac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a9b2053..22790c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/docs/en/faq.md b/docs/en/faq.md new file mode 100644 index 0000000..871120f --- /dev/null +++ b/docs/en/faq.md @@ -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. diff --git a/docs/en/migration-v0.3.1.md b/docs/en/migration-v0.3.1.md index 2db97a1..083031c 100644 --- a/docs/en/migration-v0.3.1.md +++ b/docs/en/migration-v0.3.1.md @@ -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" }, diff --git a/docs/en/obfuscation.md b/docs/en/obfuscation.md index 8548566..cdc2607 100644 --- a/docs/en/obfuscation.md +++ b/docs/en/obfuscation.md @@ -52,4 +52,10 @@ 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. \ No newline at end of file +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`. \ No newline at end of file diff --git a/docs/ru/faq.md b/docs/ru/faq.md new file mode 100644 index 0000000..e9367bf --- /dev/null +++ b/docs/ru/faq.md @@ -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), чтобы не перегружать открытую кодовую базу. diff --git a/docs/ru/migration-v0.3.1.md b/docs/ru/migration-v0.3.1.md index 0c23de4..c8b6888 100644 --- a/docs/ru/migration-v0.3.1.md +++ b/docs/ru/migration-v0.3.1.md @@ -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" }, diff --git a/docs/ru/obfuscation.md b/docs/ru/obfuscation.md index 49de434..87d94fd 100644 --- a/docs/ru/obfuscation.md +++ b/docs/ru/obfuscation.md @@ -52,4 +52,10 @@ $$\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 соединений. \ No newline at end of file +OSTP предоставляет собственную реализацию протокола XTLS-Reality без сторонних зависимостей. Протокол полностью имитирует рукопожатие TLS 1.3 (с реалистичным профилем ClientHello) для обхода продвинутых DPI фильтров. После успешного рукопожатия применяется ChaCha20Poly1305 для бесшовного шифрования и туннелирования внутренних HTTP/WSS соединений. + +--- + +## Невозможность создания статических фильтров (Защита от DPI) + +Благодаря строгой математической базе генерации энтропии, протокол полностью лишен открытых сигнатур. Это гарантирует, что системы фильтрации (например, ТСПУ от РКН или "Великий китайский файрвол") **физически не могут** написать эффективное правило блокировки, анализируя содержимое пакетов. Любая попытка написать фильтр приведет к блокировке легитимного, случайного UDP-трафика (например, WebRTC или игрового трафика). Безопасность базируется на принципе Керкгоффса — знание всех алгоритмов не помогает взломать или классифицировать трафик без `access_key`. \ No newline at end of file diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index b9c2e33..ce31ec1 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -15,6 +15,8 @@ pub struct ClientConfig { pub routing: RoutingConfig, #[serde(default, skip_serializing_if = "Option::is_none")] pub gui: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -302,6 +304,10 @@ impl ClientConfig { if let Some(gui) = json.get("gui") { new_json["gui"] = gui.clone(); } + + if let Some(api) = json.get("api") { + new_json["api"] = api.clone(); + } (new_json, true) } diff --git a/ostp-client/src/tunnel/outbounds/ostp.rs b/ostp-client/src/tunnel/outbounds/ostp.rs index 4d90cb9..5698c5c 100644 --- a/ostp-client/src/tunnel/outbounds/ostp.rs +++ b/ostp-client/src/tunnel/outbounds/ostp.rs @@ -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, diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 4ce5f55..73b4ff5 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -227,6 +227,13 @@ async fn get_config() -> Result { "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", diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 66a215b..7057fed 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -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; diff --git a/ostp-jni/src/lib.rs b/ostp-jni/src/lib.rs index 48bf64e..9bada78 100644 --- a/ostp-jni/src/lib.rs +++ b/ostp-jni/src/lib.rs @@ -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; diff --git a/ostp-server/Cargo.toml b/ostp-server/Cargo.toml index 2cff111..ee406b5 100644 --- a/ostp-server/Cargo.toml +++ b/ostp-server/Cargo.toml @@ -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 diff --git a/ostp-server/src/dispatcher.rs b/ostp-server/src/dispatcher.rs index 5052aa8..dbfe7e6 100644 --- a/ostp-server/src/dispatcher.rs +++ b/ostp-server/src/dispatcher.rs @@ -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, u64>, roaming_tokens: f64, last_token_regen: std::time::Instant, + max_sessions: Option, } #[allow(dead_code)] impl Dispatcher { - pub fn new(machine_config: ProtocolConfig, access_keys: Arc>>) -> Self { + pub fn new(machine_config: ProtocolConfig, access_keys: Arc>>, max_sessions: Option) -> 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); } diff --git a/ostp-server/src/lib.rs b/ostp-server/src/lib.rs index 072cd9b..42d350f 100644 --- a/ostp-server/src/lib.rs +++ b/ostp-server/src/lib.rs @@ -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, config_path: Option, + license_key: Option, ) -> 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(); diff --git a/ostp-server/src/license.rs b/ostp-server/src/license.rs new file mode 100644 index 0000000..ccc5053 --- /dev/null +++ b/ostp-server/src/license.rs @@ -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, +} + +#[derive(Debug)] +pub enum LicenseError { + InvalidFormat, + InvalidSignature, + Expired, + InvalidHost, + DecodeError, +} + +pub fn verify_license(license_key: &str, current_host: &str) -> Result { + 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) +} diff --git a/ostp/Cargo.toml b/ostp/Cargo.toml index 9334264..54380c7 100644 --- a/ostp/Cargo.toml +++ b/ostp/Cargo.toml @@ -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"] } diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 56f9a05..274075d 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -80,20 +80,20 @@ fn parse_ostp_link(link: &str) -> Result { 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, transport: Option, dns: Option, + license_key: Option, } /// Конфигурация Relay-узла в config.json @@ -581,8 +582,9 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { } #[cfg(windows)] { - println!(" {} Client (connect to a server via VPN/proxy)", "[1]".cyan().bold()); - println!(" {} Server (accept client connections)", "[2]".cyan().bold()); + 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::().unwrap_or(1).max(1); let mut access_keys: Vec = 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"); - wizard_register_systemd(&actual_path)?; + #[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()); diff --git a/scripts/build.ps1 b/scripts/build.ps1 index fab2b9c..dcd5d49 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -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 + '"' diff --git a/test.json b/test.json deleted file mode 100644 index 46b134b..0000000 --- a/test.json +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/test_addr.rs b/test_addr.rs deleted file mode 100644 index 6722f57..0000000 --- a/test_addr.rs +++ /dev/null @@ -1,3 +0,0 @@ -use std::net::SocketAddr; fn main() { println!(\ -:? -\, \[::1]:80\.parse::()); }