From 4970b661db8ac3a75718f55f6ab3cb808c36aa0e Mon Sep 17 00:00:00 2001 From: ospab Date: Sat, 16 May 2026 18:20:53 +0300 Subject: [PATCH] chore: implement keep-alive, config comments, validation and CI/CD improvements --- .github/workflows/release.yml | 32 +++- Cargo.lock | 3 + ostp-client/Cargo.toml | 1 + ostp-client/src/bridge.rs | 14 +- ostp-client/src/runner.rs | 46 ++++-- ostp-gui/package.json | 5 +- ostp-gui/src-tauri/src/lib.rs | 287 ++++++++++++---------------------- ostp-tun-helper/Cargo.toml | 1 + ostp-tun-helper/src/main.rs | 103 +++++------- ostp/src/main.rs | 143 +++++++++++------ 10 files changed, 325 insertions(+), 310 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cf3475..1572c1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,6 +99,7 @@ jobs: target: riscv64gc-unknown-linux-gnu artifact_name: ostp release_name: ostp-linux-riscv64.tar.gz + tun2socks_arch: linux-riscv64 use_cross: true # ========================================== @@ -138,7 +139,13 @@ jobs: - name: Execute Standard Native Compilation (Windows) if: ${{ !matrix.use_cross && matrix.os == 'windows-latest' }} - run: cargo build --release --target ${{ matrix.target }} --bin ostp + run: | + cd ostp-gui + npm install + npx tauri build --no-bundle --target ${{ matrix.target }} + cd .. + cargo build --release --target ${{ matrix.target }} --bin ostp + cargo build --release --target ${{ matrix.target }} --bin ostp-tun-helper - name: Execute Standard Native Compilation (Unix) if: ${{ !matrix.use_cross && matrix.os != 'windows-latest' }} @@ -186,11 +193,24 @@ jobs: if: ${{ matrix.os == 'windows-latest' }} shell: pwsh run: | - cd target/${{ matrix.target }}/release - $files = @("${{ matrix.artifact_name }}") - if (Test-Path "tun2socks.exe") { $files += "tun2socks.exe" } - if (Test-Path "wintun.dll") { $files += "wintun.dll" } - Compress-Archive -Path $files -DestinationPath ../../../${{ matrix.release_name }} + $build_dir = "target/${{ matrix.target }}/release" + $staging = "staging_dir" + New-Item -ItemType Directory -Path "$staging/gui" -Force + New-Item -ItemType Directory -Path "$staging/ostp" -Force + + # 1. Fill OSTP (CLI) folder + Copy-Item "$build_dir/ostp.exe" -Destination "$staging/ostp/" + if (Test-Path "$build_dir/tun2socks.exe") { Copy-Item "$build_dir/tun2socks.exe" -Destination "$staging/ostp/" } + if (Test-Path "$build_dir/wintun.dll") { Copy-Item "$build_dir/wintun.dll" -Destination "$staging/ostp/" } + + # 2. Fill GUI folder + Copy-Item "$build_dir/ostp-gui.exe" -Destination "$staging/gui/" + Copy-Item "$build_dir/ostp-tun-helper.exe" -Destination "$staging/gui/" + if (Test-Path "$build_dir/tun2socks.exe") { Copy-Item "$build_dir/tun2socks.exe" -Destination "$staging/gui/" } + if (Test-Path "$build_dir/wintun.dll") { Copy-Item "$build_dir/wintun.dll" -Destination "$staging/gui/" } + + # 3. Zip it up + Compress-Archive -Path "$staging/*" -DestinationPath "${{ matrix.release_name }}" -Force - name: Package release artifact (Unix Systems) if: ${{ matrix.os != 'windows-latest' }} diff --git a/Cargo.lock b/Cargo.lock index b74f296..c68ef70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,8 +578,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -2692,6 +2694,7 @@ name = "ostp-tun-helper" version = "0.1.43" dependencies = [ "anyhow", + "chrono", "ostp-client", "portable-atomic", "serde", diff --git a/ostp-client/Cargo.toml b/ostp-client/Cargo.toml index 75766d6..12316b3 100644 --- a/ostp-client/Cargo.toml +++ b/ostp-client/Cargo.toml @@ -14,3 +14,4 @@ rand.workspace = true serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" portable-atomic.workspace = true +chrono = "0.4" diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index bb4a29d..db0aa90 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -105,7 +105,7 @@ impl Bridge { proxy_tx: mpsc::Sender<(u16, ProxyToClientMsg)>, ) -> Result<()> { let mut metrics_tick = interval(Duration::from_millis(500)); - let mut keepalive_tick = tokio::time::interval(Duration::from_secs(10)); + let mut keepalive_tick = tokio::time::interval(Duration::from_secs(5)); let mut retransmit_tick = tokio::time::interval(Duration::from_millis(50)); let init_msg = if self.mode == "tun" { "Bridge & TUN Tunnel Manager initialized".to_string() @@ -413,6 +413,18 @@ impl Bridge { } } } + _ = keepalive_tick.tick(), if self.running => { + if let Some(sessions) = sessions_opt.as_mut() { + for session in sessions.iter_mut() { + let payload = Bytes::from(RelayMessage::KeepAlive.encode()); + // stream_id 0 is reserved for control messages + if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, payload)) { + let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await; + self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed); + } + } + } + } udp_msg = async { match udp_rx_opt.as_mut() { Some(rx) => rx.recv().await, diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index db40484..7114c97 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -6,6 +6,14 @@ use crate::bridge::{Bridge, BridgeMetrics}; use crate::signal::wait_for_shutdown_signal; use crate::tunnel; use std::sync::Arc; +use std::fs::OpenOptions; +use std::io::Write as _; + +fn log_to_core_file(msg: &str) { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("ostp-core.log") { + let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); + } +} #[cfg(target_os = "windows")] #[link(name = "kernel32")] @@ -139,6 +147,8 @@ pub async fn run_client_core( return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.")); } + log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode)); + if config.mode == "tun" && !config.exclusions.processes.is_empty() { println!("[ostp-client] WARNING: process exclusions are not supported in the current TUN implementation"); } @@ -168,6 +178,7 @@ pub async fn run_client_core( match msg { crate::app::UiEvent::Log(text) => { if debug_enabled || is_essential_log(&text) { + log_to_core_file(&format!("[client] {text}")); println!("[client] {text}"); } } @@ -227,16 +238,31 @@ pub async fn run_client_core( None }; - // Wait for external / UI shutdown signal - let _ = shutdown_rx_ext.changed().await; - - let _ = cmd_tx.send(BridgeCommand::Shutdown).await; - - let _ = shutdown_tx.send(true); - let _ = bridge_task.await?; - let _ = proxy_task.await?; - if let Some(task) = wintun_task { - let _ = task.await?; + // Wait for either external shutdown OR any task to fail + tokio::select! { + _ = shutdown_rx_ext.changed() => { + let _ = cmd_tx.send(BridgeCommand::Shutdown).await; + let _ = shutdown_tx.send(true); + let _ = bridge_task.await; + let _ = proxy_task.await; + if let Some(task) = wintun_task { + let _ = task.await; + } + } + res = bridge_task => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??; + } + res = proxy_task => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??; + } + res = async { + if let Some(t) = wintun_task { t.await } else { std::future::pending().await } + } => { + let _ = shutdown_tx.send(true); + res.map_err(|e| anyhow::anyhow!("TUN task panicked: {}", e))??; + } } Ok(()) diff --git a/ostp-gui/package.json b/ostp-gui/package.json index a108f38..2192331 100644 --- a/ostp-gui/package.json +++ b/ostp-gui/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "tauri": "tauri", - "dev": "cargo build -p ostp-tun-helper && tauri dev", - "build": "cargo build -p ostp-tun-helper --release && tauri build" + "dev": "cargo build -p ostp-tun-helper && npx tauri dev", + "build": "cargo build -p ostp-tun-helper --release && npx tauri build --no-bundle", + "build:installer": "cargo build -p ostp-tun-helper --release && npx tauri build" }, "devDependencies": { "@tauri-apps/cli": "^2" diff --git a/ostp-gui/src-tauri/src/lib.rs b/ostp-gui/src-tauri/src/lib.rs index 12dcd3e..aaf1860 100644 --- a/ostp-gui/src-tauri/src/lib.rs +++ b/ostp-gui/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::{watch, Mutex}; use tokio::task::JoinHandle; @@ -83,18 +83,14 @@ enum HelperMsg { // ── Application state ───────────────────────────────────────────────────────── -// For proxy (non-TUN) mode: runs in-process. struct InProcessState { shutdown_tx: Option>, metrics: Arc, handle: JoinHandle>, } -// For TUN mode: communicates with the privileged helper via named pipe. struct HelperState { - /// Shared state updated by pipe reader task pipe_state: Arc>, - /// Send commands to helper over named pipe cmd_tx: tokio::sync::mpsc::Sender, } @@ -133,6 +129,41 @@ fn get_config_path() -> PathBuf { PathBuf::from("config.json") } +fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::config::ClientConfig { + let turn_cfg = raw.turn.as_ref(); + ostp_client::config::ClientConfig { + mode: mode.to_string(), + debug: raw.debug.unwrap_or(false), + ostp: ostp_client::config::OstpConfig { + server_addr: raw.server.clone(), + local_bind_addr: "0.0.0.0:0".to_string(), + access_key: raw.access_key.clone(), + handshake_timeout_ms: 5000, + io_timeout_ms: 5000, + }, + local_proxy: ostp_client::config::LocalProxyConfig { + bind_addr: raw.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()), + connect_timeout_ms: 5000, + }, + turn: ostp_client::config::TurnConfig { + enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), + server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(), + username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), + access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), + }, + exclusions: ostp_client::config::ExclusionConfig { + domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(), + ips: raw.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(), + processes: raw.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(), + }, + multiplex: ostp_client::config::MultiplexConfig { + enabled: raw.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false), + sessions: raw.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1), + }, + dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()), + } +} + // ── Tauri commands ──────────────────────────────────────────────────────────── #[tauri::command] @@ -140,15 +171,37 @@ async fn get_config() -> Result { let path = get_config_path(); if !path.exists() { return Ok(r#"{ + "_comment": "OSTP Client Configuration", "mode": "client", "log_level": "info", + + "_comment_server": "Address of the remote OSTP server", "server": "127.0.0.1:50000", + + "_comment_access_key": "Must match one of the access_keys on the server", "access_key": "your-secret-access-key-hex-or-base64", + + "_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)", "socks5_bind": "127.0.0.1:1088", + + "_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)", "tun": { - "enable": true, + "enable": false, "wintun_path": "./wintun.dll", - "ipv4_address": "10.1.0.2/24" + "ipv4_address": "10.1.0.2/24", + "dns": "1.1.1.1" + }, + + "_comment_exclude": "Bypass tunnel for these domains/IPs (only works in proxy mode)", + "exclude": { + "domains": ["localhost", "127.0.0.1"], + "ips": [], + "processes": [] + }, + + "mux": { + "enabled": false, + "sessions": 1 }, "debug": false }"#.into()); @@ -171,9 +224,7 @@ async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result Ok(0), Some(TunnelHandle::InProcess(s)) => { - if s.handle.is_finished() { - return Ok(0); - } + if s.handle.is_finished() { return Ok(0); } Ok(s.metrics.connection_state.load(Ordering::Relaxed)) } Some(TunnelHandle::Helper(h)) => { @@ -208,9 +259,7 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result match guard.tunnel.take() { None => {} Some(TunnelHandle::InProcess(mut s)) => { - if let Some(tx) = s.shutdown_tx.take() { - let _ = tx.send(true); - } + if let Some(tx) = s.shutdown_tx.take() { let _ = tx.send(true); } drop(s.handle); } Some(TunnelHandle::Helper(h)) => { @@ -224,79 +273,38 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result { let mut guard = state.0.lock().await; - // Already running? - match &guard.tunnel { - Some(TunnelHandle::InProcess(s)) if !s.handle.is_finished() => return Ok(true), - Some(TunnelHandle::Helper(_)) => return Ok(true), - _ => {} + if let Some(ref t) = guard.tunnel { + match t { + TunnelHandle::InProcess(s) if !s.handle.is_finished() => return Ok(true), + TunnelHandle::Helper(_) => return Ok(true), + _ => {} + } } - // Clean up finished handle guard.tunnel = None; let path = get_config_path(); - if !path.exists() { - return Err("config.json not found. Go to Settings and configure your connection first.".into()); - } - let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; - let unified: UnifiedConfig = serde_json::from_str(&content) - .map_err(|e| format!("Config parse error: {}", e))?; + let unified: UnifiedConfig = serde_json::from_str(&content).map_err(|e| format!("Config parse error: {}", e))?; let client_cfg = match unified.mode { AppMode::Client(c) => c, - AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client mode.".into()), + AppMode::Server(_) => return Err("GUI only supports Client mode.".into()), }; let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false); if is_tun_enabled { - // ── TUN mode: launch privileged helper ──────────────────────────────── - start_tun_via_helper(&mut guard, client_cfg, content).await + start_tun_via_helper(&mut guard, &client_cfg).await } else { - // ── Proxy mode: run in-process ──────────────────────────────────────── - start_proxy_in_process(&mut guard, client_cfg).await + start_proxy_in_process(&mut guard, &client_cfg).await } } -// ── In-process proxy tunnel ────────────────────────────────────────────────── - async fn start_proxy_in_process( guard: &mut AppStateInner, - client_cfg: ClientConfigRaw, + raw: &ClientConfigRaw, ) -> Result { - let turn_cfg = client_cfg.turn.as_ref(); - let mapped = ostp_client::config::ClientConfig { - mode: "proxy".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: 5000, - }, - 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, - }, - turn: ostp_client::config::TurnConfig { - enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false), - server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(), - username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(), - access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(), - }, - 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), - }, - dns_server: None, - }; - + let mapped = map_to_client_config(raw, "proxy"); let metrics = Arc::new(BridgeMetrics { bytes_sent: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0), @@ -320,114 +328,67 @@ async fn start_proxy_in_process( Ok(true) } -// ── Privileged TUN helper via named pipe ───────────────────────────────────── - -const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper"; - async fn start_tun_via_helper( guard: &mut AppStateInner, - _client_cfg: ClientConfigRaw, - raw_config_json: String, + raw: &ClientConfigRaw, ) -> Result { - // Find the helper binary next to our exe - let helper_exe = find_helper_exe().ok_or_else(|| { - "ostp-tun-helper.exe not found next to the application. Please reinstall.".to_string() - })?; - - // Launch with UAC elevation via ShellExecuteW("runas") + let helper_exe = find_helper_exe().ok_or_else(|| "ostp-tun-helper.exe not found.".to_string())?; launch_as_admin(&helper_exe).map_err(|e| format!("Failed to launch helper: {}", e))?; - - // Give the helper time to start and create the pipe tokio::time::sleep(std::time::Duration::from_millis(1500)).await; - // Connect to the helper's named pipe - let pipe = tokio::time::timeout( - std::time::Duration::from_secs(10), - async { - loop { - match tokio::net::windows::named_pipe::ClientOptions::new().open(PIPE_NAME) { - Ok(p) => return Ok::<_, std::io::Error>(p), - Err(_) => tokio::time::sleep(std::time::Duration::from_millis(200)).await, - } + let socket = tokio::time::timeout(std::time::Duration::from_secs(60), async { + loop { + match tokio::net::TcpStream::connect("127.0.0.1:53211").await { + Ok(s) => return Ok::<_, std::io::Error>(s), + Err(_) => tokio::time::sleep(std::time::Duration::from_millis(200)).await, } } - ).await.map_err(|_| "Timed out connecting to TUN helper. It may have been denied by UAC.".to_string())? - .map_err(|e| format!("Pipe connection error: {}", e))?; + }).await.map_err(|_| "Timeout connecting to helper.".to_string())? + .map_err(|e| e.to_string())?; - // Build the config JSON and send start command - let mut mapped_config = serde_json::from_str::(&raw_config_json) - .map_err(|e| e.to_string())?; - // Ensure mode is set - if let Some(obj) = mapped_config.as_object_mut() { - obj.insert("mode".to_string(), serde_json::Value::String("tun".to_string())); - } + // Send the correctly MAPPED config + let mapped = map_to_client_config(raw, "tun"); let start_cmd = serde_json::json!({ "cmd": "start", - "config": serde_json::to_string(&mapped_config).unwrap_or_default() + "config": serde_json::to_string(&mapped).unwrap_or_default() }).to_string(); - // Set up channel for sending commands to helper task let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::(16); - - // Spawn a task that manages the pipe I/O - let pipe_state: Arc> = Arc::new(Mutex::new(HelperPipeState { - connection_state: 1, - bytes_sent: 0, - bytes_recv: 0, - })); + let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0 })); let state_for_task = pipe_state.clone(); tokio::spawn(async move { - use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; - use tokio::io::split; - - let (reader_half, mut writer_half) = split(pipe); + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, split}; + let (reader_half, mut writer_half) = split(socket); let mut reader = BufReader::new(reader_half); - - // Send the start command let _ = writer_half.write_all(format!("{}\n", start_cmd).as_bytes()).await; - // Concurrently: read from pipe, write commands from channel let mut line = String::new(); loop { tokio::select! { result = reader.read_line(&mut line) => { - let n = result.unwrap_or(0); - if n == 0 { break; } // Helper disconnected + if result.unwrap_or(0) == 0 { break; } let trimmed = line.trim().to_string(); line.clear(); - if trimmed.is_empty() { continue; } if let Ok(msg) = serde_json::from_str::(&trimmed) { let mut s = state_for_task.lock().await; match msg { HelperMsg::Status { value } => s.connection_state = value, - HelperMsg::Metrics { bytes_sent, bytes_recv } => { - s.bytes_sent = bytes_sent; - s.bytes_recv = bytes_recv; - } - HelperMsg::Error { .. } => s.connection_state = 0, - HelperMsg::Log { .. } => {} + HelperMsg::Metrics { bytes_sent, bytes_recv } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; } + HelperMsg::Error { message } => { s.connection_state = 0; eprintln!("Helper error: {}", message); } + _ => {} } } } cmd = cmd_rx.recv() => { - match cmd { - Some(c) => { let _ = writer_half.write_all(c.as_bytes()).await; } - None => break, - } + if let Some(c) = cmd { let _ = writer_half.write_all(c.as_bytes()).await; } else { break; } } } } - // Mark stopped - let mut s = state_for_task.lock().await; - s.connection_state = 0; + state_for_task.lock().await.connection_state = 0; }); - guard.tunnel = Some(TunnelHandle::Helper(HelperState { - pipe_state, - cmd_tx, - })); - + guard.tunnel = Some(TunnelHandle::Helper(HelperState { pipe_state, cmd_tx })); Ok(true) } @@ -438,16 +399,10 @@ struct HelperPipeState { } fn find_helper_exe() -> Option { - // The helper is always built to the same target dir as the GUI exe. - // In dev mode: target/debug/ostp-tun-helper.exe (same dir as ostp-gui.exe) - // In release: target/release/ostp-tun-helper.exe (same dir as ostp-gui.exe) - // In installed build: next to ostp-gui.exe if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { let candidate = dir.join("ostp-tun-helper.exe"); - if candidate.exists() { - return Some(candidate); - } + if candidate.exists() { return Some(candidate); } } } None @@ -458,61 +413,25 @@ fn launch_as_admin(exe: &PathBuf) -> Result<()> { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use std::ptr::null_mut; - let exe_wstr: Vec = exe.as_os_str().encode_wide().chain(Some(0)).collect(); let verb_wstr: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); - - #[link(name = "shell32")] - extern "system" { - fn ShellExecuteW( - hwnd: *mut std::ffi::c_void, - lpOperation: *const u16, - lpFile: *const u16, - lpParameters: *const u16, - lpDirectory: *const u16, - nShowCmd: i32, - ) -> isize; - } - - let ret = unsafe { - ShellExecuteW( - null_mut(), - verb_wstr.as_ptr(), - exe_wstr.as_ptr(), - null_mut(), - null_mut(), - 0, // SW_HIDE - ) - }; - - if ret <= 32 { - anyhow::bail!("ShellExecuteW failed (code {}). UAC was denied or helper not found.", ret); - } + #[link(name = "shell32")] extern "system" { fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize; } + let dir_wstr: Vec = exe.parent().unwrap_or(Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect(); + let ret = unsafe { ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), null_mut(), dir_wstr.as_ptr(), 0) }; + if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); } Ok(()) } #[cfg(not(target_os = "windows"))] -fn launch_as_admin(_exe: &PathBuf) -> Result<()> { - anyhow::bail!("TUN mode via helper is only supported on Windows"); -} - -// ── Tauri setup ─────────────────────────────────────────────────────────────── +fn launch_as_admin(_exe: &PathBuf) -> Result<()> { anyhow::bail!("Windows only."); } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let state = AppState(Mutex::new(AppStateInner { tunnel: None })); - tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(state) - .invoke_handler(tauri::generate_handler![ - start_tunnel, - stop_tunnel, - get_tunnel_status, - get_metrics, - get_config, - save_config - ]) + .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, get_tunnel_status, get_metrics, get_config, save_config]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/ostp-tun-helper/Cargo.toml b/ostp-tun-helper/Cargo.toml index 26662c3..c618782 100644 --- a/ostp-tun-helper/Cargo.toml +++ b/ostp-tun-helper/Cargo.toml @@ -14,6 +14,7 @@ anyhow = { workspace = true } serde = { version = "1", features = ["derive"] } serde_json = "1" portable-atomic = { workspace = true } +chrono = "0.4" [build-dependencies] # no extra build deps needed; manifest is embedded via build.rs diff --git a/ostp-tun-helper/src/main.rs b/ostp-tun-helper/src/main.rs index 671c550..509fa76 100644 --- a/ostp-tun-helper/src/main.rs +++ b/ostp-tun-helper/src/main.rs @@ -1,26 +1,24 @@ // ostp-tun-helper/src/main.rs -// -// Privileged helper for TUN mode. Runs with Administrator rights. -// Communicates with ostp-gui via a named pipe IPC channel. -// -// Protocol over the named pipe (newline-delimited JSON): -// GUI -> Helper: {"cmd":"start","config":} -// GUI -> Helper: {"cmd":"stop"} -// Helper -> GUI: {"type":"status","value":0|1|2} (0=stopped,1=connecting,2=connected) -// Helper -> GUI: {"type":"log","message":"..."} -// Helper -> GUI: {"type":"metrics","bytes_sent":N,"bytes_recv":N} - #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use anyhow::Result; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::time::Duration; +use std::fs::OpenOptions; +use std::io::Write as _; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::sync::{watch, Mutex}; +use tokio::net::TcpListener; use portable_atomic::Ordering; -const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper"; +fn log_to_file(msg: &str) { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("ostp-helper.log") { + let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); + } +} + +const BIND_ADDR: &str = "127.0.0.1:53211"; #[derive(Deserialize)] #[serde(tag = "cmd", rename_all = "lowercase")] @@ -45,43 +43,42 @@ struct TunnelState { #[tokio::main] async fn main() -> Result<()> { - // The helper is always launched by the GUI. If no client connects within - // 60 seconds, exit to avoid lingering admin processes. - run_pipe_server().await + log_to_file("Helper started (TCP mode)"); + if let Err(e) = run_server().await { + log_to_file(&format!("Fatal error: {}", e)); + } + log_to_file("Helper exiting"); + Ok(()) } -async fn run_pipe_server() -> Result<()> { - use tokio::net::windows::named_pipe::{ServerOptions}; - +async fn run_server() -> Result<()> { let state = Arc::new(Mutex::new(TunnelState { shutdown_tx: None, metrics: None, })); - // Create the named pipe server - let server = ServerOptions::new() - .first_pipe_instance(true) - .create(PIPE_NAME)?; + log_to_file(&format!("Attempting to bind to {}", BIND_ADDR)); + let listener = TcpListener::bind(BIND_ADDR).await.map_err(|e| { + log_to_file(&format!("Bind failed: {}", e)); + e + })?; + log_to_file("Listening successfully"); // Wait for GUI to connect (60 second timeout) - let connect_timeout = tokio::time::timeout( - Duration::from_secs(60), - server.connect() - ).await; - - let pipe = match connect_timeout { - Ok(Ok(())) => server, + let (socket, _) = match tokio::time::timeout(Duration::from_secs(60), listener.accept()).await { + Ok(Ok(s)) => s, _ => { - // No client connected — exit silently + log_to_file("No connection from GUI within 60s, exiting"); return Ok(()); } }; - let (reader_half, writer_half) = tokio::io::split(pipe); + log_to_file("GUI connected via TCP"); + + let (reader_half, writer_half) = tokio::io::split(socket); let writer = Arc::new(Mutex::new(writer_half)); let mut reader = BufReader::new(reader_half); - // Helper to send a message back to GUI let send_msg = { let writer = writer.clone(); move |msg: HelperMsg| { @@ -94,13 +91,12 @@ async fn run_pipe_server() -> Result<()> { } }; - // Read commands from GUI let mut line = String::new(); loop { line.clear(); let n = reader.read_line(&mut line).await.unwrap_or(0); if n == 0 { - // GUI disconnected — stop tunnel and exit + log_to_file("GUI disconnected, stopping tunnel"); let mut st = state.lock().await; if let Some(tx) = st.shutdown_tx.take() { let _ = tx.send(true); @@ -109,9 +105,7 @@ async fn run_pipe_server() -> Result<()> { } let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } + if trimmed.is_empty() { continue; } let cmd: GuiCmd = match serde_json::from_str(trimmed) { Ok(c) => c, @@ -123,7 +117,7 @@ async fn run_pipe_server() -> Result<()> { match cmd { GuiCmd::Start { config } => { - // Stop any existing tunnel first + log_to_file("Received START command"); { let mut st = state.lock().await; if let Some(tx) = st.shutdown_tx.take() { @@ -132,10 +126,10 @@ async fn run_pipe_server() -> Result<()> { tokio::time::sleep(Duration::from_millis(100)).await; } - // Parse config let cfg: ostp_client::config::ClientConfig = match serde_json::from_str(&config) { Ok(c) => c, Err(e) => { + log_to_file(&format!("Config parse error: {}", e)); send_msg(HelperMsg::Error { message: format!("Config parse error: {}", e) }); continue; } @@ -155,37 +149,23 @@ async fn run_pipe_server() -> Result<()> { st.metrics = Some(metrics.clone()); } - // Spawn the tunnel let metrics_for_runner = metrics.clone(); - let send_log = { - let writer = writer.clone(); - move |msg: String| { - let writer = writer.clone(); - let json = serde_json::to_string(&HelperMsg::Log { message: msg }).unwrap_or_default(); - tokio::spawn(async move { - let mut w = writer.lock().await; - let _ = w.write_all(format!("{}\n", json).as_bytes()).await; - }); - } - }; - - let writer_for_tick = writer.clone(); - let metrics_for_tick = metrics.clone(); - + let writer_for_err = writer.clone(); tokio::spawn(async move { + log_to_file("Starting tunnel core..."); match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx).await { - Ok(_) => {} + Ok(_) => { log_to_file("Tunnel core stopped normally"); } Err(e) => { + log_to_file(&format!("Tunnel core error: {}", e)); let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() }).unwrap_or_default(); - let mut w = writer_for_tick.lock().await; + let mut w = writer_for_err.lock().await; let _ = w.write_all(format!("{}\n", json).as_bytes()).await; } } }); - // Spawn a tick that forwards status + metrics to GUI every second let writer_tick = writer.clone(); - let metrics_tick = metrics_for_tick.clone(); + let metrics_tick = metrics.clone(); tokio::spawn(async move { let mut last_state = 99u8; loop { @@ -195,13 +175,11 @@ async fn run_pipe_server() -> Result<()> { let recv = metrics_tick.bytes_recv.load(Ordering::Relaxed); let mut w = writer_tick.lock().await; - // Only send status change events if cs != last_state { last_state = cs; let json = serde_json::to_string(&HelperMsg::Status { value: cs }).unwrap_or_default(); if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; } } - // Always send metrics let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv }).unwrap_or_default(); if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; } drop(w); @@ -210,8 +188,8 @@ async fn run_pipe_server() -> Result<()> { send_msg(HelperMsg::Status { value: 1 }); } - GuiCmd::Stop => { + log_to_file("Received STOP command"); let mut st = state.lock().await; if let Some(tx) = st.shutdown_tx.take() { let _ = tx.send(true); @@ -221,6 +199,5 @@ async fn run_pipe_server() -> Result<()> { } } } - Ok(()) } diff --git a/ostp/src/main.rs b/ostp/src/main.rs index 9a15c3d..fa94190 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -103,6 +103,34 @@ struct UnifiedConfig { log_level: Option, } +impl UnifiedConfig { + fn validate(&self) -> Result<()> { + match &self.mode { + AppMode::Server(cfg) => { + if cfg.access_keys.is_empty() { + anyhow::bail!("Server configuration must contain at least one access_key."); + } + if let Some(outbound) = &cfg.outbound { + if outbound.enabled { + let action = outbound.default_action.as_deref().unwrap_or("direct"); + if action == "direct" && outbound.rules.is_empty() { + println!("\n[WARNING] Server outbound proxy is ENABLED, but default_action is 'direct' and there are no rules!"); + println!(" This means ALL traffic will bypass the proxy and go out directly from the server IP."); + println!(" If you want all traffic to be proxied, change 'default_action' to 'proxy'.\n"); + } + } + } + } + AppMode::Client(cfg) => { + if cfg.access_key.is_empty() { + anyhow::bail!("Client configuration must contain an access_key."); + } + } + } + Ok(()) + } +} + #[derive(Debug, Deserialize, Serialize)] struct ServerConfig { listen: String, @@ -284,52 +312,77 @@ async fn run_app() -> Result<()> { // Handle explicit configuration initialization if let Some(ref mode_str) = args.init { let is_server = mode_str == "server"; - let dummy = if is_server { - UnifiedConfig { - log_level: Some("info".to_string()), - mode: AppMode::Server(ServerConfig { - listen: "0.0.0.0:50000".to_string(), - access_keys: vec![generate_secure_key("hex")], - turn_server: None, - debug: Some(false), - outbound: Some(OutboundConfig { - enabled: false, - protocol: "".to_string(), - address: "".to_string(), - port: 0, - rules: Vec::new(), - default_action: Some("direct".to_string()), - }), - }), - } + let key = generate_secure_key("hex"); + let content = if is_server { + format!(r#"{{ + "_comment": "OSTP Server Configuration", + "mode": "server", + "log_level": "info", + + "_comment_listen": "The address and port the server listens on for incoming OSTP connections.", + "listen": "0.0.0.0:50000", + + "_comment_access_keys": "List of valid keys. Clients must use one of these to connect.", + "access_keys": [ + "{}" + ], + + "_comment_outbound": "Optional proxy for outbound traffic. If enabled, the server routes traffic through this proxy.", + "outbound": {{ + "enabled": false, + "protocol": "socks5", + "address": "127.0.0.1", + "port": 9050, + "_comment_default_action": "Can be 'proxy' (route all traffic through proxy by default) or 'direct' (bypass proxy by default).", + "default_action": "proxy", + "_comment_rules": "Specific routing rules. Action can be 'proxy', 'direct', or 'block'.", + "rules": [ + {{ + "domain_suffix": [".onion"], + "action": "proxy" + }} + ] + }}, + "debug": false +}}"#, key) } else { - UnifiedConfig { - log_level: Some("info".to_string()), - mode: AppMode::Client(ClientConfig { - server: "127.0.0.1:50000".to_string(), - access_key: generate_secure_key("hex"), - socks5_bind: Some("127.0.0.1:1088".to_string()), - tun: Some(TunConfig { - enable: false, - wintun_path: Some("./wintun.dll".to_string()), - ipv4_address: Some("10.1.0.2/24".to_string()), - dns: None, - }), - turn: None, - debug: Some(false), - exclude: Some(ExcludeConfig { - domains: Some(Vec::new()), - ips: Some(Vec::new()), - processes: Some(Vec::new()), - }), - mux: Some(MuxConfig { - enabled: Some(false), - sessions: Some(1), - }), - }), - } + format!(r#"{{ + "_comment": "OSTP Client Configuration", + "mode": "client", + "log_level": "info", + + "_comment_server": "Address of the remote OSTP server", + "server": "127.0.0.1:50000", + + "_comment_access_key": "Must match one of the access_keys on the server", + "access_key": "{}", + + "_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)", + "socks5_bind": "127.0.0.1:1088", + + "_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)", + "tun": {{ + "enable": false, + "wintun_path": "./wintun.dll", + "ipv4_address": "10.1.0.2/24", + "dns": "1.1.1.1" + }}, + + "_comment_exclude": "Bypass tunnel for these domains/IPs (only works in proxy mode)", + "exclude": {{ + "domains": ["localhost", "127.0.0.1"], + "ips": [], + "processes": [] + }}, + + "mux": {{ + "enabled": false, + "sessions": 1 + }}, + "debug": false +}}"#, key) }; - fs::write(&args.config, serde_json::to_string_pretty(&dummy)?)?; + fs::write(&args.config, content)?; println!("Successfully initialized configuration at {:?}", args.config); if is_server { @@ -361,6 +414,8 @@ async fn run_app() -> Result<()> { let config: UnifiedConfig = serde_json::from_str(&config_content) .map_err(|e| anyhow!("Failed to parse config: {}", e))?; + config.validate()?; + if args.links { match config.mode { AppMode::Server(server_cfg) => {