mirror of https://github.com/ospab/ostp.git
feat: unified cross-platform TUN support (Linux + Windows Firewall dynamic bypass)
This commit is contained in:
parent
f4c8a7d6bc
commit
22fb9bb3d3
|
|
@ -476,7 +476,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp"
|
name = "ostp"
|
||||||
version = "0.1.21"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -491,7 +491,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.1.21"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -506,7 +506,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.1.21"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|
@ -538,7 +538,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-server"
|
name = "ostp-server"
|
||||||
version = "0.1.21"
|
version = "0.1.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> {
|
||||||
let wintun_shutdown_rx = shutdown_tx.subscribe();
|
let wintun_shutdown_rx = shutdown_tx.subscribe();
|
||||||
let wintun_task = if config_clone.mode == "tun" {
|
let wintun_task = if config_clone.mode == "tun" {
|
||||||
Some(tokio::spawn(async move {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -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<bool>,
|
||||||
|
) -> 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<bool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
Err(anyhow!("Linux tunnel driver executed on a non-Linux host!"))
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,32 @@
|
||||||
mod proxy;
|
mod proxy;
|
||||||
mod wintun_downloader;
|
mod wintun_downloader;
|
||||||
mod wintun_handler;
|
mod wintun_handler;
|
||||||
|
mod linux_handler;
|
||||||
|
|
||||||
pub use wintun_downloader::download_wintun_dll;
|
pub use wintun_downloader::download_wintun_dll;
|
||||||
pub use wintun_downloader::download_tun2socks;
|
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<bool>,
|
||||||
|
) -> 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};
|
use tokio::sync::{mpsc, watch};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,55 @@ pub fn download_tun2socks(debug: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn download_tun2socks(_debug: bool) -> Result<()> {
|
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(())
|
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."))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,14 +42,19 @@ pub async fn run_wintun_tunnel(
|
||||||
println!("[ostp-client] Injecting system routing tables and excluding remote proxy...");
|
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!(
|
let setup_script = format!(
|
||||||
"$remote_ip = '{}'\n\
|
"$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\
|
$gw = $route.NextHop\n\
|
||||||
$ifIndex = $route.InterfaceIndex\n\
|
$ifIndex = $route.InterfaceIndex\n\
|
||||||
New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\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",
|
New-NetRoute -DestinationPrefix \"1.1.1.1/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||||
server_ip_str
|
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")
|
let out = Command::new("powershell")
|
||||||
|
|
@ -133,7 +138,8 @@ pub async fn run_wintun_tunnel(
|
||||||
let cleanup_script = format!(
|
let cleanup_script = format!(
|
||||||
"$remote_ip = '{}'\n\
|
"$remote_ip = '{}'\n\
|
||||||
Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\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
|
server_ip_str
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -146,10 +152,3 @@ pub async fn run_wintun_tunnel(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub async fn run_wintun_tunnel(
|
|
||||||
_config: crate::config::ClientConfig,
|
|
||||||
_shutdown: watch::Receiver<bool>,
|
|
||||||
) -> Result<()> {
|
|
||||||
Err(anyhow!("Wintun is only supported on Windows!"))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue