diff --git a/Cargo.lock b/Cargo.lock index 5241e40..f7421dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,7 +476,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "ostp" -version = "0.1.21" +version = "0.1.26" dependencies = [ "anyhow", "base64", @@ -491,7 +491,7 @@ dependencies = [ [[package]] name = "ostp-client" -version = "0.1.21" +version = "0.1.26" dependencies = [ "anyhow", "bytes", @@ -506,7 +506,7 @@ dependencies = [ [[package]] name = "ostp-core" -version = "0.1.21" +version = "0.1.26" dependencies = [ "anyhow", "async-trait", @@ -538,7 +538,7 @@ dependencies = [ [[package]] name = "ostp-server" -version = "0.1.21" +version = "0.1.26" dependencies = [ "anyhow", "bytes", diff --git a/ostp-client/src/runner.rs b/ostp-client/src/runner.rs index d1e3b01..8aadb8a 100644 --- a/ostp-client/src/runner.rs +++ b/ostp-client/src/runner.rs @@ -177,7 +177,7 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> { let wintun_shutdown_rx = shutdown_tx.subscribe(); let wintun_task = if config_clone.mode == "tun" { Some(tokio::spawn(async move { - tunnel::run_wintun_tunnel(config_clone, wintun_shutdown_rx).await + tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx).await })) } else { None diff --git a/ostp-client/src/tunnel/linux_handler.rs b/ostp-client/src/tunnel/linux_handler.rs new file mode 100644 index 0000000..d257b7d --- /dev/null +++ b/ostp-client/src/tunnel/linux_handler.rs @@ -0,0 +1,183 @@ +use anyhow::{anyhow, Result}; +use tokio::sync::watch; + +#[cfg(target_os = "linux")] +use std::net::ToSocketAddrs; +#[cfg(target_os = "linux")] +use std::process::{Command, Stdio}; +#[cfg(target_os = "linux")] +use std::io::{BufRead, BufReader}; + +#[cfg(target_os = "linux")] +pub async fn run_linux_tunnel( + config: crate::config::ClientConfig, + mut shutdown: watch::Receiver, +) -> Result<()> { + let debug = config.debug; + if debug { + println!("[ostp-client] Starting Linux TUN handler initialization..."); + } + + // 1. Locate tun2socks binary + let exe = std::env::current_exe()?; + let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?; + + let mut tun2socks_exe = dir.join("tun2socks"); + if !tun2socks_exe.exists() { + // Try system PATH via standard command check + let in_path = Command::new("which") + .arg("tun2socks") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + + if in_path { + tun2socks_exe = std::path::PathBuf::from("tun2socks"); + } else { + return Err(anyhow!("tun2socks executable not found in local dir or PATH. Please ensure dependencies are present.")); + } + } + + // 2. Resolve Server IP for routing table exclusion + let server_ip = config.ostp.server_addr.to_socket_addrs() + .map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))? + .next() + .map(|addr| addr.ip()) + .ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?; + + let server_ip_str = server_ip.to_string(); + + if debug { + println!("[ostp-client] Resolved remote server IP: {}", server_ip_str); + } + + // 3. Detect current default gateway and interface + let route_output = Command::new("sh") + .arg("-c") + .arg("ip route show default | head -n1") + .output()?; + + let route_str = String::from_utf8_lossy(&route_output.stdout); + let parts: Vec<&str> = route_str.split_whitespace().collect(); + + // Expected: "default via 192.168.1.1 dev eth0 ..." + let mut default_gw = String::new(); + let mut default_if = String::new(); + + for i in 0..parts.len() { + if parts[i] == "via" && i + 1 < parts.len() { + default_gw = parts[i+1].to_string(); + } + if parts[i] == "dev" && i + 1 < parts.len() { + default_if = parts[i+1].to_string(); + } + } + + if default_gw.is_empty() || default_if.is_empty() { + return Err(anyhow!("Failed to discover active default gateway or network interface on Linux system.")); + } + + if debug { + println!("[ostp-client] Physical route anchor: gateway={} interface={}", default_gw, default_if); + } + + // 4. Setup commands (Using standard /1 routing trick for fail-proof overriding) + let setup_script = format!( + "ip tuntap add name ostp_tun mode tun || true; \ + ip addr add 10.1.0.2/24 dev ostp_tun || true; \ + ip link set dev ostp_tun up; \ + ip route add {} via {} dev {}; \ + ip route add 1.1.1.1 via {} dev {}; \ + ip route add 0.0.0.0/1 dev ostp_tun; \ + ip route add 128.0.0.0/1 dev ostp_tun", + server_ip_str, default_gw, default_if, + default_gw, default_if + ); + + if debug { + println!("[ostp-client] Executing Linux network config: {}", setup_script); + } + + let out = Command::new("sh") + .args(["-c", &setup_script]) + .output()?; + + if !out.status.success() && debug { + println!("[ostp-client] Warning: Setup routing returned: {}", String::from_utf8_lossy(&out.stderr)); + } + + // 5. Prepare and launch tun2socks + let proxy_url = format!("socks5://{}", config.local_proxy.bind_addr); + + if debug { + println!("[ostp-client] Spawning {} -device ostp_tun -proxy {}", tun2socks_exe.display(), proxy_url); + } + + let mut child = Command::new(&tun2socks_exe) + .args([ + "-device", "ostp_tun", + "-proxy", &proxy_url, + ]) + .stdout(if debug { Stdio::piped() } else { Stdio::null() }) + .stderr(if debug { Stdio::piped() } else { Stdio::null() }) + .spawn() + .map_err(|e| anyhow!("Failed to spawn tun2socks process: {}", e))?; + + println!("[client] TUN Tunnel established, Linux traffic is now routing through OSTP."); + + if debug { + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + tokio::spawn(async move { + let reader = BufReader::new(stdout); + for line in reader.lines().map_while(Result::ok) { + println!("[tun2socks] {}", line); + } + }); + + tokio::spawn(async move { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + eprintln!("[tun2socks-err] {}", line); + } + }); + } + + // 6. Wait for shutdown signal + let _ = shutdown.changed().await; + + println!("[client] Deactivating TUN tunnel and restoring Linux network topology..."); + + // 7. Terminate process + let _ = child.kill(); + + // 8. Cleanup routing and virtual interface + let cleanup_script = format!( + "ip route del 0.0.0.0/1 dev ostp_tun || true; \ + ip route del 128.0.0.0/1 dev ostp_tun || true; \ + ip route del {} via {} dev {} || true; \ + ip route del 1.1.1.1 via {} dev {} || true; \ + ip link set dev ostp_tun down || true; \ + ip tuntap del name ostp_tun mode tun || true", + server_ip_str, default_gw, default_if, + default_gw, default_if + ); + + let _ = Command::new("sh") + .args(["-c", &cleanup_script]) + .output()?; + + println!("[client] Linux TUN Tunnel stopped."); + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +#[allow(dead_code)] +pub async fn run_linux_tunnel( + _config: crate::config::ClientConfig, + _shutdown: watch::Receiver, +) -> Result<()> { + Err(anyhow!("Linux tunnel driver executed on a non-Linux host!")) +} diff --git a/ostp-client/src/tunnel/mod.rs b/ostp-client/src/tunnel/mod.rs index 246dd04..1467da5 100644 --- a/ostp-client/src/tunnel/mod.rs +++ b/ostp-client/src/tunnel/mod.rs @@ -1,10 +1,32 @@ mod proxy; mod wintun_downloader; mod wintun_handler; +mod linux_handler; pub use wintun_downloader::download_wintun_dll; pub use wintun_downloader::download_tun2socks; -pub use wintun_handler::run_wintun_tunnel; + +pub async fn run_tun_tunnel( + config: crate::config::ClientConfig, + shutdown: watch::Receiver, +) -> anyhow::Result<()> { + #[cfg(target_os = "windows")] + { + wintun_handler::run_wintun_tunnel(config, shutdown).await + } + + #[cfg(target_os = "linux")] + { + linux_handler::run_linux_tunnel(config, shutdown).await + } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + { + let _ = shutdown; + let _ = config; + anyhow::bail!("Operating system unsupported, text an issue at github."); + } +} use tokio::sync::{mpsc, watch}; diff --git a/ostp-client/src/tunnel/wintun_downloader.rs b/ostp-client/src/tunnel/wintun_downloader.rs index 703c868..8150567 100644 --- a/ostp-client/src/tunnel/wintun_downloader.rs +++ b/ostp-client/src/tunnel/wintun_downloader.rs @@ -112,7 +112,55 @@ pub fn download_tun2socks(debug: bool) -> Result<()> { Ok(()) } -#[cfg(not(target_os = "windows"))] -pub fn download_tun2socks(_debug: bool) -> Result<()> { +#[cfg(target_os = "linux")] +pub fn download_tun2socks(debug: bool) -> Result<()> { + let exe = std::env::current_exe()?; + let dir = exe.parent().ok_or_else(|| anyhow::anyhow!("failed to get binary directory"))?; + let tun2socks_path = dir.join("tun2socks"); + + if !tun2socks_path.exists() { + if debug { + println!("[ostp-client] tun2socks not found. Downloading automatically for Linux..."); + } + + let arch = if cfg!(target_arch = "x86_64") { + "amd64" + } else if cfg!(target_arch = "aarch64") { + "arm64" + } else if cfg!(target_arch = "arm") { + "arm" + } else { + "386" + }; + + let tar_path = dir.join("tun2socks.tar.gz").to_string_lossy().into_owned(); + let dest_path = tun2socks_path.to_string_lossy().into_owned(); + let url = format!("https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-linux-{}.tar.gz", arch); + + let sh_script = format!( + "curl -L -o '{}' '{}' && tar -xzf '{}' -C '{}' --wildcards '*/tun2socks' --strip-components=1 || tar -xzf '{}' -C '{}' tun2socks; \ + chmod +x '{}'; \ + rm -f '{}'", + tar_path, url, tar_path, dir.to_string_lossy(), tar_path, dir.to_string_lossy(), dest_path, tar_path + ); + + let output = std::process::Command::new("sh") + .args(["-c", &sh_script]) + .current_dir(dir) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("Failed to download tun2socks for Linux: {}", stderr)); + } + if debug { + println!("[ostp-client] tun2socks ({}) downloaded and installed successfully!", arch); + } + } Ok(()) } + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub fn download_tun2socks(_debug: bool) -> Result<()> { + Err(anyhow::anyhow!("Operating system unsupported, text an issue at github.")) +} diff --git a/ostp-client/src/tunnel/wintun_handler.rs b/ostp-client/src/tunnel/wintun_handler.rs index 5b6b804..558438b 100644 --- a/ostp-client/src/tunnel/wintun_handler.rs +++ b/ostp-client/src/tunnel/wintun_handler.rs @@ -42,14 +42,19 @@ pub async fn run_wintun_tunnel( println!("[ostp-client] Injecting system routing tables and excluding remote proxy..."); } + let current_exe = std::env::current_exe()?.to_string_lossy().into_owned(); + let setup_script = format!( "$remote_ip = '{}'\n\ - $route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Sort-Object RouteMetric | Select-Object -First 1\n\ + $exe_path = '{}'\n\ + $route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {{ $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' }} | Sort-Object RouteMetric | Select-Object -First 1\n\ $gw = $route.NextHop\n\ $ifIndex = $route.InterfaceIndex\n\ New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\ - New-NetRoute -DestinationPrefix \"1.1.1.1/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n", - server_ip_str + New-NetRoute -DestinationPrefix \"1.1.1.1/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\ + New-NetFirewallRule -DisplayName 'OSTP Tunnel In' -Direction Inbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\ + New-NetFirewallRule -DisplayName 'OSTP Tunnel Out' -Direction Outbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n", + server_ip_str, current_exe ); let out = Command::new("powershell") @@ -133,7 +138,8 @@ pub async fn run_wintun_tunnel( let cleanup_script = format!( "$remote_ip = '{}'\n\ Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\ - Remove-NetRoute -DestinationPrefix \"1.1.1.1/32\" -Confirm:$false -ErrorAction SilentlyContinue\n", + Remove-NetRoute -DestinationPrefix \"1.1.1.1/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\ + Remove-NetFirewallRule -DisplayName 'OSTP Tunnel*' -ErrorAction SilentlyContinue\n", server_ip_str ); @@ -146,10 +152,3 @@ pub async fn run_wintun_tunnel( Ok(()) } -#[cfg(not(target_os = "windows"))] -pub async fn run_wintun_tunnel( - _config: crate::config::ClientConfig, - _shutdown: watch::Receiver, -) -> Result<()> { - Err(anyhow!("Wintun is only supported on Windows!")) -}