mirror of https://github.com/ospab/ostp.git
227 lines
6.9 KiB
Rust
227 lines
6.9 KiB
Rust
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;
|
|
|
|
#[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")]
|
|
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<()> {
|
|
let current_exe = std::env::current_exe()?;
|
|
let exe_str = current_exe.to_string_lossy().replace('\'', "''");
|
|
|
|
let current_dir = std::env::current_dir()?;
|
|
let dir_str = current_dir.to_string_lossy().replace('\'', "''");
|
|
|
|
let mut arg_parts = Vec::new();
|
|
for arg in std::env::args().skip(1) {
|
|
arg_parts.push(format!("'{}'", arg.replace('\'', "''")));
|
|
}
|
|
|
|
let arg_list_str = if !arg_parts.is_empty() {
|
|
format!("-ArgumentList {}", arg_parts.join(","))
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let ps_script = format!(
|
|
"Start-Process -FilePath '{}' {} -WorkingDirectory '{}' -Verb RunAs",
|
|
exe_str, arg_list_str, dir_str
|
|
);
|
|
|
|
let _ = std::process::Command::new("powershell")
|
|
.args(["-Command", &ps_script])
|
|
.spawn()?;
|
|
std::process::exit(0);
|
|
}
|
|
|
|
pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> {
|
|
let bg = std::env::args().any(|a| a == "--bg");
|
|
|
|
if bg {
|
|
hide_console();
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
if config.mode == "tun" && !is_admin() {
|
|
println!("[ostp-client] TUN mode requires Administrator privileges. Relaunching as Admin...");
|
|
relaunch_as_admin()?;
|
|
}
|
|
|
|
if config.mode == "tun" && !config.exclusions.processes.is_empty() {
|
|
println!("[ostp-client] WARNING: process exclusions are not supported in the current TUN implementation");
|
|
}
|
|
|
|
if config.mode == "tun" {
|
|
tunnel::download_wintun_dll(config.debug)?;
|
|
tunnel::download_tun2socks(config.debug)?;
|
|
}
|
|
|
|
let (proxy_events_tx, proxy_events_rx) = mpsc::channel(10000);
|
|
let (client_msgs_tx, client_msgs_rx) = mpsc::channel(10000);
|
|
|
|
let metrics = Arc::new(BridgeMetrics {
|
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
|
bytes_recv: portable_atomic::AtomicU64::new(0),
|
|
});
|
|
|
|
let bridge = Bridge::new(&config, metrics)?;
|
|
|
|
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();
|
|
|
|
let is_tun = config.mode == "tun";
|
|
|
|
// 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) {
|
|
println!("[client] {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!("[client] status={status_str} rtt_ms={:.1}", rtt_ms);
|
|
}
|
|
}
|
|
crate::app::UiEvent::Traffic { .. } => {}
|
|
crate::app::UiEvent::ProfileChanged(profile) => {
|
|
if debug_enabled {
|
|
println!("[client] profile={profile:?}");
|
|
}
|
|
}
|
|
crate::app::UiEvent::TunnelStopped => {
|
|
if is_tun {
|
|
println!("[client] tunnel=tun stopped, reconnecting in 5s");
|
|
} else {
|
|
println!("[client] tunnel=proxy stopped, reconnecting in 5s");
|
|
}
|
|
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 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_task = tokio::spawn(async move {
|
|
tunnel::run_local_proxy(
|
|
config.local_proxy,
|
|
config.ostp,
|
|
config.exclusions,
|
|
config.debug,
|
|
proxy_shutdown_rx,
|
|
proxy_events_tx,
|
|
client_msgs_rx,
|
|
)
|
|
.await
|
|
});
|
|
|
|
let wintun_shutdown_rx = shutdown_tx.subscribe();
|
|
let wintun_task = if config_clone.mode == "tun" {
|
|
Some(tokio::spawn(async move {
|
|
tunnel::run_tun_tunnel(config_clone, wintun_shutdown_rx).await
|
|
}))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Wait for Ctrl-C / signal
|
|
wait_for_shutdown_signal().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?;
|
|
}
|
|
tunnel::cleanup().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,
|
|
"Handshaking started"
|
|
| "Bridge connection established"
|
|
| "TUN Tunnel established"
|
|
| "Bridge stopped"
|
|
| "TUN Tunnel stopped"
|
|
| "Runtime config reloaded"
|
|
) || text.starts_with("Connected UDP directly to ")
|
|
|| text.starts_with("TURN: Relay allocated")
|
|
|| text.starts_with("TURN allocation failed")
|
|
|| text.starts_with("Handshake failed")
|
|
|| text.starts_with("Connection timeout")
|
|
}
|