feat(cli): add --import, --proxy-env, interactive link prompt, and TUN safety guard for Linux

This commit is contained in:
ospab 2026-05-31 20:53:54 +03:00
parent eb0a193fee
commit ba5fe72873
2 changed files with 131 additions and 2 deletions

View File

@ -15,6 +15,27 @@ pub async fn run_native_tunnel(
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
#[cfg(target_os = "linux")]
{
use std::io::{self, IsTerminal, Write};
if io::stdout().is_terminal() {
println!("\n===================================================================");
println!("WARNING: TUN mode will modify the system routing table.");
println!("If you are connected to a headless server via SSH, you may lose");
println!("your connection when default routes are redirected into the tunnel.");
println!("===================================================================\n");
print!("Are you sure you want to initialize the TUN interface? [yes/no]: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let ans = input.trim().to_lowercase();
if ans != "y" && ans != "yes" {
return Err(anyhow!("TUN initialization aborted by user. Run without TUN to use as a local proxy."));
}
}
}
let debug = config.debug; let debug = config.debug;
tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)..."); tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)...");
@ -31,7 +52,7 @@ pub async fn run_native_tunnel(
.address((10, 1, 0, 2)) .address((10, 1, 0, 2))
.netmask((255, 255, 255, 0)) .netmask((255, 255, 255, 0))
.destination((10, 1, 0, 1)) .destination((10, 1, 0, 1))
.mtu(config.ostp.mtu.saturating_sub(48).max(500) as u16) .mtu(config.ostp.mtu as u16)
.up(); .up();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View File

@ -46,6 +46,18 @@ struct Args {
/// Update OSTP: re-run the install script to fetch and install the latest version /// Update OSTP: re-run the install script to fetch and install the latest version
#[arg(long)] #[arg(long)]
update: bool, update: bool,
/// Import a share link (ostp://...) into the configuration file and exit
#[arg(long)]
import: Option<String>,
/// Output shell export commands for proxy (eval $(ostp --proxy-env))
#[arg(long)]
proxy_env: bool,
/// Output shell export commands to clear proxy (eval $(ostp --proxy-env-clear))
#[arg(long)]
proxy_env_clear: bool,
} }
fn parse_ostp_link(link: &str) -> Result<ClientConfig> { fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
@ -512,6 +524,35 @@ async fn run_app() -> Result<()> {
return cmd_update(); return cmd_update();
} }
if args.proxy_env {
let mut port = 1088;
if args.config.exists() {
if let Ok(content) = fs::read_to_string(&args.config) {
let mut stripped = json_comments::StripComments::new(content.as_bytes());
if let Ok(config) = serde_json::from_reader::<_, UnifiedConfig>(&mut stripped) {
if let AppMode::Client(c) = config.mode {
if let Some(bind) = c.socks5_bind {
if let Some(p) = bind.split(':').last().and_then(|s| s.parse::<u16>().ok()) {
port = p;
}
}
}
}
}
}
println!("export http_proxy=\"socks5://127.0.0.1:{}\"", port);
println!("export https_proxy=\"socks5://127.0.0.1:{}\"", port);
println!("export all_proxy=\"socks5://127.0.0.1:{}\"", port);
return Ok(());
}
if args.proxy_env_clear {
println!("unset http_proxy");
println!("unset https_proxy");
println!("unset all_proxy");
return Ok(());
}
if args.generate_key { if args.generate_key {
let mut new_keys = Vec::new(); let mut new_keys = Vec::new();
for _ in 0..args.count { for _ in 0..args.count {
@ -548,10 +589,77 @@ async fn run_app() -> Result<()> {
return Ok(()); return Ok(());
} }
if let Some(import_url) = args.import {
println!("[ostp] Importing configuration from share link...");
let client_cfg = parse_ostp_link(&import_url)
.map_err(|e| anyhow!("Share Link Error: {e}"))?;
let unified = UnifiedConfig {
mode: AppMode::Client(client_cfg),
log_level: Some("info".to_string()),
};
let content = serde_json::to_string_pretty(&unified)?;
if let Some(parent) = args.config.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
fs::write(&args.config, content)?;
println!("[ostp] Configuration successfully imported and saved to {:?}", args.config);
return Ok(());
}
if let Some(url) = args.url { if let Some(url) = args.url {
println!("[ostp] Connecting via share link..."); println!("[ostp] Connecting via share link...");
let client_cfg = parse_ostp_link(&url) let mut client_cfg = parse_ostp_link(&url)
.map_err(|e| anyhow!("Share Link Error: {e}"))?; .map_err(|e| anyhow!("Share Link Error: {e}"))?;
// Interactive prompt for URL launch
use std::io::Write;
print!("Enable TUN (VPN) mode? [y/N]: ");
std::io::stdout().flush().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
if input.trim().eq_ignore_ascii_case("y") {
if let Some(tun) = &mut client_cfg.tun {
tun.enable = true;
}
}
print!("Enable connection multiplexing (mux)? [y/N]: ");
std::io::stdout().flush().unwrap();
input.clear();
std::io::stdin().read_line(&mut input).unwrap();
if input.trim().eq_ignore_ascii_case("y") {
print!("How many sessions? [5]: ");
std::io::stdout().flush().unwrap();
input.clear();
std::io::stdin().read_line(&mut input).unwrap();
let mut sessions = 5;
if !input.trim().is_empty() {
if let Ok(s) = input.trim().parse() {
sessions = s;
}
}
if client_cfg.mux.is_none() {
client_cfg.mux = Some(MuxConfig {
enabled: Some(true),
sessions: Some(sessions),
});
} else if let Some(mux) = &mut client_cfg.mux {
mux.enabled = Some(true);
mux.sessions = Some(sessions);
}
}
print!("Enable debug mode? [y/N]: ");
std::io::stdout().flush().unwrap();
input.clear();
std::io::stdin().read_line(&mut input).unwrap();
if input.trim().eq_ignore_ascii_case("y") {
client_cfg.debug = Some(true);
}
return run_client_directly(client_cfg).await; return run_client_directly(client_cfg).await;
} }