use anyhow::Result; use tokio::sync::{mpsc, watch}; use crate::app::BridgeCommand; 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) { let path = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("ostp-core.log"))) .unwrap_or_else(|| std::path::PathBuf::from("ostp-core.log")); if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); } } #[cfg(target_os = "windows")] #[link(name = "kernel32")] extern "system" { fn FreeConsole() -> i32; fn GetConsoleWindow() -> *mut std::ffi::c_void; } #[cfg(target_os = "windows")] #[link(name = "user32")] extern "system" { fn ShowWindow(hwnd: *mut std::ffi::c_void, cmd_show: i32) -> i32; } fn hide_console() { #[cfg(target_os = "windows")] unsafe { let hwnd = GetConsoleWindow(); if !hwnd.is_null() { ShowWindow(hwnd, 0); // SW_HIDE = 0 } FreeConsole(); } } #[cfg(target_os = "windows")] pub fn is_admin() -> bool { std::process::Command::new("net") .arg("session") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false) } #[cfg(target_os = "windows")] fn relaunch_as_admin() -> Result<()> { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use std::ptr::null_mut; let exe = std::env::current_exe()?; let exe_wstr: Vec = exe.as_os_str().encode_wide().chain(Some(0)).collect(); let mut args_joined = String::new(); for arg in std::env::args().skip(1) { if !args_joined.is_empty() { args_joined.push(' '); } args_joined.push('"'); args_joined.push_str(&arg.replace('"', "\\\"")); args_joined.push('"'); } let args_wstr: Vec = OsStr::new(&args_joined).encode_wide().chain(Some(0)).collect(); let dir = std::env::current_dir()?; let dir_wstr: Vec = dir.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; } unsafe { let ret = ShellExecuteW( null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), args_wstr.as_ptr(), dir_wstr.as_ptr(), 1, // SW_SHOWNORMAL = 1 ); if ret <= 32 { return Err(anyhow::anyhow!( "Windows UAC Elevation failed or was denied by policy (ShellExecuteW code: {})", ret )); } } std::process::exit(0); } #[cfg(target_os = "linux")] pub fn is_root() -> bool { unsafe { libc::geteuid() == 0 } } #[cfg(target_os = "linux")] fn relaunch_as_root() -> Result<()> { use std::io::IsTerminal; let exe = std::env::current_exe()?; let args: Vec = std::env::args().skip(1).collect(); let is_gui = std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok(); let is_term = std::io::stdout().is_terminal(); let mut cmd = if is_gui && !is_term { let mut c = std::process::Command::new("pkexec"); c.arg(exe); c } else { let mut c = std::process::Command::new("sudo"); c.arg(exe); c }; cmd.args(&args); let status = cmd.status().map_err(|e| anyhow::anyhow!("Failed to execute privilege escalation command: {}", e))?; if !status.success() { return Err(anyhow::anyhow!("Privilege escalation failed or was denied.")); } std::process::exit(0); } pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { #[cfg(target_os = "windows")] if config.mode == "tun" && !is_admin() { println!("[ostp] TUN mode requires administrator privileges. Relaunching..."); relaunch_as_admin()?; } #[cfg(target_os = "linux")] if config.mode == "tun" && !is_root() { println!("[ostp] TUN mode requires root privileges. Requesting sudo/pkexec elevation..."); relaunch_as_root()?; } let bg = std::env::args().any(|a| a == "--bg"); if bg { hide_console(); } let metrics = Arc::new(BridgeMetrics { 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), }); let (shutdown_tx, shutdown_rx) = watch::channel(false); tokio::spawn(async move { if wait_for_shutdown_signal().await.is_ok() { let _ = shutdown_tx.send(true); } }); run_client_core(config, metrics, shutdown_rx, None).await } pub async fn run_client_core( mut config: crate::config::ClientConfig, metrics: Arc, mut shutdown_rx_ext: watch::Receiver, mut config_rx: Option>, ) -> Result<()> { #[cfg(target_os = "windows")] if config.mode == "tun" && !is_admin() { return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.")); } #[cfg(target_os = "linux")] if config.mode == "tun" && !is_root() { return Err(anyhow::anyhow!("Root privileges are required to initialize TUN mode on Linux. Please run with sudo.")); } log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode)); // Resolve the server IP before we override system routing and DNS. // This prevents DNS deadlock if the VPN disconnects and tries to reconnect, // and also ensures we add the direct route to the exact IP the bridge connects to. #[allow(unused_mut)] let mut resolved_addrs: Vec = tokio::net::lookup_host(&config.ostp.server_addr) .await .map_err(|e| anyhow::anyhow!("Failed to resolve server address {}: {}", config.ostp.server_addr, e))? .collect(); let target_addr = resolved_addrs.first() .ok_or_else(|| anyhow::anyhow!("No IP addresses resolved for {}", config.ostp.server_addr))?; log_to_core_file(&format!("[core] Resolved server address to {}", target_addr)); config.ostp.server_addr = target_addr.to_string(); #[cfg(target_os = "linux")] if config.mode == "tun" { println!("\n[ostp] ==========================================================================="); println!("[ostp] WARNING: You are starting TUN mode on a Linux system."); println!("[ostp] If this is a remote headless server, routing all traffic through the TUN"); println!("[ostp] interface WILL DROP your SSH connection and lock you out!"); println!("[ostp] "); println!("[ostp] SOLUTION: Add a static route for your client IP to bypass the TUN."); println!("[ostp] Find your default gateway (ip route | grep default) and run:"); println!("[ostp] sudo ip route add via "); println!("[ostp] ===========================================================================\n"); } #[cfg(target_os = "linux")] if config.mode == "proxy" { println!("\n[ostp] ==========================================================================="); println!("[ostp] Proxy mode initialized on {}", config.local_proxy.bind_addr); println!("[ostp] ===========================================================================\n"); } let _sysproxy_guard = if config.mode == "proxy" { Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr)) } else { None }; if config.mode == "tun" && !config.exclusions.processes.is_empty() { println!("[ostp] Process exclusions are not supported in TUN mode"); } let (proxy_events_tx, proxy_events_rx) = mpsc::channel(256); let (client_msgs_tx, client_msgs_rx) = mpsc::unbounded_channel(); // Setup exclusions hot-reload channel let (reload_tx, reload_rx) = watch::channel(config.exclusions.clone()); let mut bridge = Bridge::new(&config, metrics)?; bridge.reload_tx = Some(reload_tx.clone()); let (ui_tx, mut ui_rx) = mpsc::channel(512); let (cmd_tx, cmd_rx) = mpsc::channel(128); let (shutdown_tx, shutdown_rx) = watch::channel(false); let proxy_shutdown_rx = shutdown_tx.subscribe(); // Auto-connect on startup let _ = cmd_tx.send(BridgeCommand::ToggleTunnel).await; let debug_enabled = config.debug; // Headless event logger let cmd_tx_clone = cmd_tx.clone(); tokio::spawn(async move { let mut last_status = None; while let Some(msg) = ui_rx.recv().await { match msg { crate::app::UiEvent::Log(text) => { if debug_enabled || is_essential_log(&text) { log_to_core_file(&format!("[ostp] {text}")); println!("[ostp] {text}"); } } crate::app::UiEvent::Metrics { status, rtt_ms, .. } => { let status_str = status.as_str().to_string(); if last_status != Some(status_str.clone()) { last_status = Some(status_str.clone()); println!("[ostp] Status: {} (rtt={:.1}ms)", status_str, rtt_ms); } } crate::app::UiEvent::Traffic { .. } => {} crate::app::UiEvent::ProfileChanged(profile) => { if debug_enabled { println!("[ostp] Obfuscation profile: {profile:?}"); } } crate::app::UiEvent::TunnelStopped => { println!("[ostp] Connection interrupted. Reconnecting in 5 seconds..."); let cmd_tx_inner = cmd_tx_clone.clone(); tokio::spawn(async move { tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; let _ = cmd_tx_inner.send(BridgeCommand::ToggleTunnel).await; }); } } } }); let mut bridge_task = tokio::spawn(async move { bridge.run(ui_tx, cmd_rx, shutdown_rx, proxy_events_rx, client_msgs_tx).await }); let config_clone = config.clone(); let proxy_exclusions_rx = reload_rx.clone(); let mut proxy_task = tokio::spawn(async move { tunnel::run_local_proxy( config.local_proxy, config.ostp, proxy_exclusions_rx, config.debug, proxy_shutdown_rx, proxy_events_tx, client_msgs_rx, ) .await }); let wintun_shutdown_rx = shutdown_tx.subscribe(); let wintun_exclusions_rx = reload_rx.clone(); let mut wintun_task = if config_clone.mode == "tun" { Some(tokio::spawn(async move { tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx, wintun_exclusions_rx).await })) } else { None }; // Wait for local_shutdown let mut local_shutdown = shutdown_rx_ext.clone(); let cmd_tx_loop = cmd_tx.clone(); tokio::spawn(async move { loop { tokio::select! { _ = local_shutdown.changed() => { if *local_shutdown.borrow() { let _ = cmd_tx_loop.send(BridgeCommand::Shutdown).await; break; } } Some(Ok(_)) = async { if let Some(ref mut rx) = config_rx { Some(rx.changed().await) } else { std::future::pending().await } } => { if let Some(ref rx) = config_rx { let new_cfg = rx.borrow().clone(); let _ = reload_tx.send(new_cfg.exclusions); } } } } }); // 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); } res = &mut bridge_task => { let _ = shutdown_tx.send(true); res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??; } res = &mut 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.as_mut() { t.await } else { std::future::pending().await } } => { let _ = shutdown_tx.send(true); res.map_err(|e| anyhow::anyhow!("TUN task panicked: {}", e))??; } } // Final cleanup: wait for tasks to finish let _ = bridge_task.await; let _ = proxy_task.await; if let Some(task) = wintun_task { let _ = task.await; } Ok(()) } #[allow(dead_code)] fn format_bytes(bps: u64) -> String { if bps >= 1_000_000 { format!("{:.1}MB", bps as f64 / 1_000_000.0) } else if bps >= 1_000 { format!("{:.1}KB", bps as f64 / 1_000.0) } else { format!("{bps}B") } } fn is_essential_log(text: &str) -> bool { matches!( text, "Connection established" | "TUN tunnel established" | "TUN tunnel stopped" | "Bridge stopped" | "Runtime config reloaded" | "Connecting to remote server..." ) || text.starts_with("Connected to ") || text.starts_with("TURN relay allocated") || text.starts_with("TURN allocation failed") || text.starts_with("Allocating TURN relay") || text.starts_with("Connection failed:") || text.starts_with("Connection lost") || text.starts_with("Protocol tick fatal error") }