mirror of https://github.com/ospab/ostp.git
feat: update build script and documentation
This commit is contained in:
parent
67f9c06935
commit
630c3fde73
|
|
@ -34,5 +34,7 @@ turn-harvesting-idea.md
|
|||
|
||||
# Private tooling (closed-source)
|
||||
ostp-prober/
|
||||
|
||||
ostp-brain/
|
||||
ostp-sandbox/
|
||||
ostp-control/
|
||||
ostp-license/
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
127.0.0.1
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -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), чтобы не перегружать открытую кодовую базу.
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 + '"'
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
use std::net::SocketAddr; fn main() { println!(\
|
||||
:?
|
||||
\, \[::1]:80\.parse::<SocketAddr>()); }
|
||||
Loading…
Reference in New Issue