From 8ed66f95531db212a8e3e71217c67c6838f197af Mon Sep 17 00:00:00 2001 From: ospab Date: Tue, 16 Jun 2026 18:09:46 +0300 Subject: [PATCH] docs: Update config format to modular architecture v0.3.1 --- MIGRATION_V0_3_1.md | 102 ++++++++++ README.md | 31 +++- README.ru.md | 57 +++--- ostp-client/src/bridge.rs | 11 ++ ostp-client/src/config.rs | 147 ++++++++++++++- ostp/src/main.rs | 378 +++++++++++++++++++++----------------- 6 files changed, 521 insertions(+), 205 deletions(-) create mode 100644 MIGRATION_V0_3_1.md diff --git a/MIGRATION_V0_3_1.md b/MIGRATION_V0_3_1.md new file mode 100644 index 0000000..b725ea3 --- /dev/null +++ b/MIGRATION_V0_3_1.md @@ -0,0 +1,102 @@ +# Миграция конфигурации OSTP v0.3.1 + +В версии OSTP 0.3.1 мы полностью переработали архитектуру конфигурации `config.json` для клиента. Старая монолитная структура (где все настройки были в корневом объекте) заменена на модульную систему на базе массивов `inbounds` (входящие соединения) и `outbounds` (исходящие соединения), аналогично Xray/V2Ray/Sing-box. + +Это позволяет OSTP масштабироваться, поддерживать несколько прокси-серверов, несколько точек входа (SOCKS5, TUN) и сложную маршрутизацию (`routing`). + +## Автоматическая миграция + +В ядро `ostp` встроен автоматический мигратор. При запуске любой программы (cli, gui, flutter) ядро проверит ваш `config.json`. + +Если в конфигурации отсутствует поле `"version": "0.3.1"`, OSTP **автоматически** конвертирует ваш старый конфиг в новый модульный формат и сохранит его на диск без потери данных. + +### Что происходит при миграции: + +1. **TUN и SOCKS5** -> преобразуются в массив `inbounds`. + - Настройка `socks5_bind` становится входящим `local_proxy` (SOCKS). + - Настройка `tun` становится входящим `tun`. +2. **Сервер OSTP** -> переносится в массив `outbounds`. + - Параметры `server`, `access_key`, `transport`, `mux` объединяются в `outbound` с типом `"ostp"`. +3. **Split Tunneling (Exclude)** -> преобразуется в `routing` правила. + - Старые `domains` и `ips` конвертируются в правила, направляющие трафик в `"direct"` outbound. + - Все остальные запросы по умолчанию направляются в `"proxy"` outbound. +4. **Поля `version`** + - Добавляется поле `"version": "0.3.1"`, чтобы предотвратить повторную миграцию в будущем. Поле `_comment` было удалено. + +## Пример изменения + +### До 0.3.1 (Старый формат) +```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"] + } +} +``` + +### После 0.3.1 (Новый формат) +```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" + } +} +``` + +## Информация для разработчиков GUI (ostp-gui, ostp-flutter) + +Если вы разрабатываете интеграции или сторонние клиенты, **вам больше не нужно парсить старые поля**. Вы должны использовать массивы `inbounds` и `outbounds`. Если GUI передает `serde_json::Value` в ядро, ядро само проведет миграцию перед запуском. Однако для сохранения изменений из UI вы должны изменять именно новую структуру массивов. diff --git a/README.md b/README.md index 8c74a50..ad99a1e 100644 --- a/README.md +++ b/README.md @@ -118,14 +118,35 @@ graph TD ```jsonc { "mode": "client", - "server": "YOUR_SERVER_IP:50000", - "access_key": "YOUR_SECRET_KEY", - "socks5_bind": "127.0.0.1:1088", - "transport": { "mode": "udp", "stealth_sni": "vk.com" }, - "tun": { "enable": false, "dns": "1.1.1.1" } + "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 } + ], + "outbounds": [ + { + "type": "ostp", + "tag": "proxy", + "server": "YOUR_SERVER_IP", + "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 ```bash diff --git a/README.ru.md b/README.ru.md index 0445759..3cb39ef 100644 --- a/README.ru.md +++ b/README.ru.md @@ -110,40 +110,37 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie ```jsonc { "mode": "client", - "server": "IP_СЕРВЕРА:50000", - "access_key": "ВАШ_КЛЮЧ", - "socks5_bind": "127.0.0.1:1088", - "debug": false, - // Настройки транспорта (udp или uot) - "transport": { - "mode": "udp", - "stealth_sni": "vk.com" - }, - // TUN-режим (полносистемный VPN) - "tun": { - "enable": false, - "dns": "1.1.1.1" - }, - // Мультиплексирование: несколько UDP-сессий - "mux": { - "enabled": false, - "sessions": 2 - }, - // TURN-реле для заблокированных сетей - "turn": { - "enabled": false, - "server_addr": "turn.example.com:3478", - "username": "user", - "access_key": "pass" - }, - // Исключения (идут напрямую, минуя туннель) - "exclude": { - "domains": ["example.local"], - "ips": ["192.168.0.0/16"] + "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 } + ], + "outbounds": [ + { + "type": "ostp", + "tag": "proxy", + "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" } } ``` +> **Примечание:** Обновляетесь с v0.2.x? Прочтите [Гайд по миграции на v0.3.1](MIGRATION_V0_3_1.md). + --- ## Использование diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 8c2e509..1a49bd0 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -7,6 +7,17 @@ pub struct BridgeMetrics { pub rtt_ms: AtomicU32, } +impl Default for BridgeMetrics { + fn default() -> Self { + Self { + bytes_sent: portable_atomic::AtomicU64::new(0), + bytes_recv: portable_atomic::AtomicU64::new(0), + connection_state: portable_atomic::AtomicU8::new(0), + rtt_ms: portable_atomic::AtomicU32::new(0), + } + } +} + pub fn set_socket_protector(f: F) where F: Fn(i32) -> bool + Send + Sync + 'static, diff --git a/ostp-client/src/config.rs b/ostp-client/src/config.rs index 9567a86..70d650e 100644 --- a/ostp-client/src/config.rs +++ b/ostp-client/src/config.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, #[serde(default)] pub log: LogConfig, #[serde(default)] @@ -153,9 +155,150 @@ impl ClientConfig { let raw = std::fs::read_to_string(&path) .with_context(|| format!("failed to read {}", path.display()))?; let mut stripped = json_comments::StripComments::new(raw.as_bytes()); - let config: ClientConfig = serde_json::from_reader(&mut stripped) - .with_context(|| format!("failed to parse {}", path.display()))?; + let raw_json: serde_json::Value = serde_json::from_reader(&mut stripped) + .with_context(|| format!("failed to parse JSON from {}", path.display()))?; + + let (migrated_json, was_migrated) = Self::migrate_json(raw_json); + + if was_migrated { + tracing::info!("Config was migrated to v0.3.1. Saving to {}", path.display()); + let serialized = serde_json::to_string_pretty(&migrated_json)?; + std::fs::write(&path, serialized) + .with_context(|| format!("failed to save migrated config to {}", path.display()))?; + } + + let config: ClientConfig = serde_json::from_value(migrated_json) + .with_context(|| format!("failed to deserialize migrated config from {}", path.display()))?; Ok(config) } + + /// Migrates old monolithic JSON to the new modular format. + /// Returns the migrated JSON value and a boolean indicating if a migration occurred. + pub fn migrate_json(mut json: serde_json::Value) -> (serde_json::Value, bool) { + let is_migrated = json.get("version").and_then(|v| v.as_str()) == Some("0.3.1"); + if is_migrated { + return (json, false); + } + + // Needs migration + let mut new_json = serde_json::json!({ + "version": "0.3.1", + }); + + // 1. Log level + let log_level = if let Some(ll) = json.get("log_level") { + ll.clone() + } else if let Some(d) = json.get("debug") { + if d.as_bool().unwrap_or(false) { serde_json::json!("debug") } else { serde_json::json!("info") } + } else { + serde_json::json!("info") + }; + new_json["log"] = serde_json::json!({ "level": log_level }); + + // 2. Inbounds + let mut inbounds = Vec::new(); + + if let Some(tun) = json.get("tun") { + if tun.get("enable").and_then(|v| v.as_bool()).unwrap_or(false) { + inbounds.push(serde_json::json!({ + "type": "tun", + "tag": "tun-in", + "auto_route": true, + "mtu": 1140 + })); + } + } + + let socks_bind = json.get("socks5_bind").and_then(|v| v.as_str()).unwrap_or("127.0.0.1:1088"); + let parts: Vec<&str> = socks_bind.split(':').collect(); + let listen = parts.get(0).unwrap_or(&"127.0.0.1"); + let port = parts.get(1).unwrap_or(&"1088").parse::().unwrap_or(1088); + + inbounds.push(serde_json::json!({ + "type": "local_proxy", + "tag": "socks-in", + "protocol": "socks", + "listen": listen, + "port": port + })); + + new_json["inbounds"] = serde_json::Value::Array(inbounds); + + // 3. Outbounds + let mut outbounds = Vec::new(); + let server_full = json.get("server").and_then(|v| v.as_str()).unwrap_or("127.0.0.1:50000"); + let server_parts: Vec<&str> = server_full.split(':').collect(); + let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1"); + let server_port = server_parts.get(1).unwrap_or(&"50000").parse::().unwrap_or(50000); + let access_key = json.get("access_key").and_then(|v| v.as_str()).unwrap_or(""); + + let transport_type = json.get("transport").and_then(|t| t.get("mode").or(t.get("type"))).and_then(|v| v.as_str()).unwrap_or("udp"); + let mux_enabled = json.get("mux").and_then(|m| m.get("enabled")).and_then(|v| v.as_bool()).unwrap_or(false); + let mux_sessions = json.get("mux").and_then(|m| m.get("sessions")).and_then(|v| v.as_u64()).unwrap_or(1); + + outbounds.push(serde_json::json!({ + "type": "ostp", + "tag": "proxy", + "server": server_host, + "port": server_port, + "access_key": access_key, + "transport": { + "type": transport_type + }, + "multiplex": { + "enabled": mux_enabled, + "sessions": mux_sessions + } + })); + + outbounds.push(serde_json::json!({ + "type": "direct", + "tag": "direct" + })); + + outbounds.push(serde_json::json!({ + "type": "block", + "tag": "block" + })); + + new_json["outbounds"] = serde_json::Value::Array(outbounds); + + // 4. Routing + let mut rules = Vec::new(); + + // Migrate exclusions to route to direct + if let Some(exclude) = json.get("exclude") { + if let Some(domains) = exclude.get("domains") { + rules.push(serde_json::json!({ + "domain_suffix": domains, + "outbound": "direct" + })); + } + if let Some(ips) = exclude.get("ips") { + rules.push(serde_json::json!({ + "ip_cidr": ips, + "outbound": "direct" + })); + } + if let Some(processes) = exclude.get("processes") { + rules.push(serde_json::json!({ + "process_name": processes, + "outbound": "direct" + })); + } + } + + new_json["routing"] = serde_json::json!({ + "rules": rules, + "default_outbound": "proxy" + }); + + // 5. Preserve GUI state + if let Some(gui) = json.get("gui") { + new_json["gui"] = gui.clone(); + } + + (new_json, true) + } } diff --git a/ostp/src/main.rs b/ostp/src/main.rs index f5e268d..56f9a05 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -65,7 +65,7 @@ struct Args { proxy_env_clear: bool, } -fn parse_ostp_link(link: &str) -> Result { +fn parse_ostp_link(link: &str) -> Result { let parsed = url::Url::parse(link) .map_err(|e| anyhow!("Failed to parse share link URL: {e}"))?; @@ -98,29 +98,55 @@ fn parse_ostp_link(link: &str) -> Result { } } - Ok(ClientConfig { - server, - access_key, - mtu: None, - transport: Some(TransportConfigRaw { - mode: Some(transport_mode), - stealth_sni: Some(sni.clone()), - wss: Some(wss_enabled), - }), - socks5_bind: Some("127.0.0.1:1088".to_string()), - tun: Some(TunConfig { - enable: tun_enabled, - wintun_path: Some("./wintun.dll".to_string()), - ipv4_address: Some("10.1.0.2/24".to_string()), - dns: tun_dns, - kill_switch: Some(false), - }), - - debug: Some(false), - exclude: None, - mux: None, - gui: None, - }) + Ok(serde_json::json!({ + "version": "0.3.1", + "log": { + "level": "info" + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "auto_route": tun_enabled, + "mtu": 1140 + }, + { + "type": "local_proxy", + "tag": "socks-in", + "protocol": "socks", + "listen": "127.0.0.1", + "port": 1088 + } + ], + "outbounds": [ + { + "type": "ostp", + "tag": "proxy", + "server": parsed.host_str().unwrap_or(""), + "port": parsed.port().unwrap_or(50000), + "access_key": access_key, + "transport": { + "type": transport_mode + }, + "multiplex": { + "enabled": false, + "sessions": 1 + } + }, + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "routing": { + "rules": [], + "default_outbound": "proxy" + } + })) } fn generate_secure_key(format_type: &str) -> String { @@ -148,7 +174,7 @@ fn parse_outbound_action(value: Option) -> ostp_server::OutboundAction { #[serde(tag = "mode", rename_all = "lowercase")] enum AppMode { Server(ServerConfig), - Client(ClientConfig), + Client(serde_json::Value), Relay(RelayServerConfig), } @@ -178,8 +204,11 @@ impl UnifiedConfig { } } AppMode::Client(cfg) => { - if cfg.access_key.is_empty() { - anyhow::bail!("Client configuration must contain an access_key."); + if let Some(outbounds) = cfg.get("outbounds").and_then(|o| o.as_array()) { + let has_proxy = outbounds.iter().any(|o| o.get("type").and_then(|t| t.as_str()) == Some("ostp")); + if !has_proxy { + anyhow::bail!("Client configuration must contain an ostp outbound proxy."); + } } } AppMode::Relay(cfg) => { @@ -222,11 +251,37 @@ impl UserConfig { pub fn limit(&self) -> Option { match self { UserConfig::KeyOnly(_) => None, - UserConfig::Detailed { limit_bytes, .. } => limit_bytes.clone(), + UserConfig::Detailed { limit_bytes, .. } => *limit_bytes, } } } +#[derive(Debug, Deserialize, Serialize)] +struct OutboundConfig { + enabled: bool, + protocol: String, + address: String, + port: u16, + #[serde(default)] + rules: Vec, + default_action: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +struct OutboundRule { + domain_suffix: Option>, + ip_cidr: Option>, + protocol: Option, + action: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct TransportConfigRaw { + mode: Option, + stealth_sni: Option, + wss: Option, +} + #[derive(Debug, Deserialize, Serialize)] struct ServerConfig { listen: ListenConfig, @@ -302,68 +357,7 @@ struct FallbackCfg { target: Option, } -#[derive(Debug, Deserialize, Serialize)] -struct ClientConfig { - server: String, - access_key: String, - mtu: Option, - socks5_bind: Option, - tun: Option, - debug: Option, - exclude: Option, - mux: Option, - transport: Option, - gui: Option, -} -#[derive(Debug, Deserialize, Serialize, Clone)] -struct TransportConfigRaw { - mode: Option, - stealth_sni: Option, - wss: Option, -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -struct TunConfig { - enable: bool, - wintun_path: Option, - ipv4_address: Option, - dns: Option, - kill_switch: Option, -} - - -#[derive(Debug, Deserialize, Serialize)] -struct OutboundConfig { - enabled: bool, - protocol: String, - address: String, - port: u16, - #[serde(default)] - rules: Vec, - default_action: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -struct OutboundRule { - domain_suffix: Option>, - ip_cidr: Option>, - protocol: Option, - action: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -struct ExcludeConfig { - domains: Option>, - ips: Option>, - processes: Option>, -} - -#[derive(Debug, Deserialize, Serialize)] -struct MuxConfig { - enabled: Option, - sessions: Option, -} #[tokio::main] async fn main() -> Result<()> { @@ -679,34 +673,68 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { let _ = key_for_gen; let _ = &sni; + let server_parts: Vec<&str> = server.split(':').collect(); + let server_host = server_parts.get(0).unwrap_or(&"127.0.0.1"); + let server_port = server_parts.get(1).unwrap_or(&"50000").parse::().unwrap_or(50000); + + let socks_parts: Vec<&str> = socks_bind.split(':').collect(); + let socks_host = socks_parts.get(0).unwrap_or(&"127.0.0.1"); + let socks_port = socks_parts.get(1).unwrap_or(&"1088").parse::().unwrap_or(1088); + let client_json = serde_json::json!({ "mode": "client", - "log_level": "info", - "server": server, - "access_key": access_key, - "socks5_bind": socks_bind, - "tun": { - "enable": tun_enable, - "wintun_path": "./wintun.dll", - "ipv4_address": "10.1.0.2/24", - "dns": tun_dns, - "kill_switch": kill_switch + "version": "0.3.1", + "log": { + "level": "info" }, - "exclude": { - "domains": ["localhost", "127.0.0.1"], - "ips": [], - "processes": [] - }, - "transport": { - "mode": transport_mode, - "stealth_sni": "www.microsoft.com", - "wss": false - }, - "mux": { - "enabled": mux_enable, - "sessions": mux_sessions - }, - "debug": false + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "auto_route": tun_enable, + "mtu": 1140 + }, + { + "type": "local_proxy", + "tag": "socks-in", + "protocol": "socks", + "listen": socks_host, + "port": socks_port + } + ], + "outbounds": [ + { + "type": "ostp", + "tag": "proxy", + "server": server_host, + "port": server_port, + "access_key": access_key, + "transport": { + "type": transport_mode + }, + "multiplex": { + "enabled": mux_enable, + "sessions": mux_sessions + } + }, + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "routing": { + "rules": [ + { + "domain_suffix": ["localhost", "127.0.0.1"], + "outbound": "direct" + } + ], + "default_outbound": "proxy" + } }); let actual_path = wizard_save_config(config_path, &client_json)?; @@ -755,6 +783,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { // intentional: step text then daemon call below let server_json = serde_json::json!({ "mode": "server", + "version": "0.3.1", "log_level": "info", "listen": listen, "access_keys": access_keys, @@ -867,6 +896,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { let panel_bind = format!("0.0.0.0:{}", panel_port); let server_json = serde_json::json!({ "mode": "server", + "version": "0.3.1", "log_level": "info", "listen": listen, "access_keys": access_keys, @@ -929,6 +959,7 @@ fn run_setup_wizard(config_path: &std::path::Path) -> Result<()> { wizard_step(2, TOTAL, "Saving configuration"); let relay_json = serde_json::json!({ "mode": "relay", + "version": "0.3.1", "listen": listen, "upstream_tcp": upstream, "upstream_udp": upstream, @@ -1060,9 +1091,15 @@ 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::Client(c) = config.mode { - if let Some(bind) = c.socks5_bind { - if let Some(p) = bind.split(':').last().and_then(|s| s.parse::().ok()) { - port = p; + let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(c); + if let Some(inbounds) = migrated.get("inbounds").and_then(|i| i.as_array()) { + for inbound in inbounds { + if inbound.get("type").and_then(|t| t.as_str()) == Some("local_proxy") { + if let Some(p) = inbound.get("port").and_then(|p| p.as_u64()) { + port = p as u16; + break; + } + } } } } @@ -1150,8 +1187,14 @@ async fn run_app() -> Result<()> { let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); if input.trim().eq_ignore_ascii_case("y") { - if let Some(tun) = &mut client_cfg.tun { - tun.enable = true; + if let Some(i_val) = client_cfg.get_mut("inbounds") { + if let Some(inbounds) = i_val.as_array_mut() { + for inbound in inbounds.iter_mut() { + if inbound.get("type").and_then(|t| t.as_str()) == Some("tun") { + inbound["auto_route"] = serde_json::json!(true); + } + } + } } } @@ -1170,14 +1213,17 @@ async fn run_app() -> Result<()> { sessions = s; } } - if client_cfg.mux.is_none() { - client_cfg.mux = Some(MuxConfig { - enabled: Some(true), - sessions: Some(sessions), - }); - } else if let Some(mux) = &mut client_cfg.mux { - mux.enabled = Some(true); - mux.sessions = Some(sessions); + if let Some(o_val) = client_cfg.get_mut("outbounds") { + if let Some(outbounds) = o_val.as_array_mut() { + for outbound in outbounds.iter_mut() { + if outbound.get("type").and_then(|t| t.as_str()) == Some("ostp") { + outbound["multiplex"] = serde_json::json!({ + "enabled": true, + "sessions": sessions + }); + } + } + } } } @@ -1186,7 +1232,7 @@ async fn run_app() -> Result<()> { input.clear(); std::io::stdin().read_line(&mut input).unwrap(); if input.trim().eq_ignore_ascii_case("y") { - client_cfg.debug = Some(true); + client_cfg["log"]["level"] = serde_json::json!("debug"); } return run_client_directly(client_cfg).await; @@ -1226,8 +1272,24 @@ async fn run_app() -> Result<()> { } AppMode::Client(c) => { println!("{} Config OK: client mode", "[ostp]".green().bold()); - println!(" Server: {}", c.server.cyan()); - println!(" Key: {}...", &c.access_key[..8.min(c.access_key.len())].yellow()); + let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(c.clone()); + let mut display_server = "unknown"; + let mut display_key = "unknown"; + if let Some(outbounds) = migrated.get("outbounds").and_then(|o| o.as_array()) { + for outbound in outbounds { + if outbound.get("type").and_then(|t| t.as_str()) == Some("ostp") { + if let Some(s) = outbound.get("server").and_then(|s| s.as_str()) { + display_server = s; + } + if let Some(k) = outbound.get("access_key").and_then(|k| k.as_str()) { + display_key = k; + } + break; + } + } + } + println!(" Server: {}", display_server.cyan()); + println!(" Key: {}...", &display_key[..8.min(display_key.len())].yellow()); } AppMode::Relay(r) => { println!("{} Config OK: relay mode", "[ostp]".green().bold()); @@ -1601,46 +1663,26 @@ fn cmd_update() -> Result<()> { anyhow::bail!("The 'update' command is only supported on Linux/Unix systems."); } -async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> { - let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); +async fn run_client_directly(client_cfg: serde_json::Value) -> Result<()> { + let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg); + let client_conf: ostp_client::config::ClientConfig = serde_json::from_value(migrated)?; + + let mut is_tun_enabled = false; + for inbound in &client_conf.inbounds { + if matches!(inbound, ostp_client::config::InboundConfig::Tun { .. }) { + is_tun_enabled = true; + break; + } + } + let mode_str = if is_tun_enabled { "tun" } else { "proxy" }; - println!("{} Starting client (mode={}, server={})", "[ostp]".cyan().bold(), mode_str.yellow(), client_cfg.server.cyan()); let client_conf = ostp_client::config::ClientConfig { - mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() }, - tun_stack: "native".to_string(), - debug: client_cfg.debug.unwrap_or(false), - ostp: ostp_client::config::OstpConfig { - server_addr: client_cfg.server.clone(), - local_bind_addr: "0.0.0.0:0".to_string(), - access_key: client_cfg.access_key.clone(), - handshake_timeout_ms: 5000, - io_timeout_ms: 2500, - mtu: client_cfg.mtu.unwrap_or(1350), - keepalive_interval_sec: 5, - }, - local_proxy: ostp_client::config::LocalProxyConfig { - bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), - connect_timeout_ms: 5000, - }, - exclusions: ostp_client::config::ExclusionConfig { - domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), - ips: client_cfg.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(), - processes: client_cfg.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(), - }, - multiplex: ostp_client::config::MultiplexConfig { - enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false), - sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1), - }, - transport: ostp_client::config::TransportConfig { - mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()), - stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()), - wss: client_cfg.transport.as_ref().and_then(|t| t.wss).unwrap_or(false), - }, - dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()), - kill_switch: client_cfg.tun.as_ref().and_then(|t| t.kill_switch).unwrap_or(false), - gui: None, - }; + println!("{} Starting client (mode={})", "[ostp]".cyan().bold(), mode_str.yellow()); // Run the client implementation - ostp_client::runner::run_client(client_conf).await?; + let (_shutdown_tx, rx) = tokio::sync::watch::channel(false); + let metrics = std::sync::Arc::new(ostp_client::bridge::BridgeMetrics::default()); + + // Launch the core runner directly. + ostp_client::runner::run_client_core(client_conf, metrics, rx, None).await?; Ok(()) }