//! OSTP Client CLI - Stealth VPN Client for Windows //! //! Usage: //! ostp-client connect --server 1.2.3.4:8443 --psk //! ostp-client --config %APPDATA%/ostp/client.json use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::{style, Emoji}; use dialoguer::{Confirm, Input, Select}; use ostp::{ClientConfig, OstpClient}; use ostp_guard::error_codes; use std::net::SocketAddr; use std::path::PathBuf; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; static ROCKET: Emoji<'_, '_> = Emoji("🚀 ", ""); static LOCK: Emoji<'_, '_> = Emoji("🔒 ", ""); static CHECK: Emoji<'_, '_> = Emoji("✅ ", "[OK] "); static WARN: Emoji<'_, '_> = Emoji("⚠️ ", "[!] "); static GLOBE: Emoji<'_, '_> = Emoji("🌍 ", ""); #[derive(Parser)] #[command(name = "ostp-client")] #[command(author = "Ospab Team")] #[command(version)] #[command(about = "OSTP Stealth VPN Client", long_about = None)] struct Cli { #[command(subcommand)] command: Option, /// Path to config file (JSON) #[arg(short, long)] config: Option, /// Log level (trace, debug, info, warn, error) #[arg(long, default_value = "info")] log_level: String, } #[derive(Subcommand)] enum Commands { /// Connect to an OSTP server Connect { /// Server address (e.g., vpn.example.com:8443) #[arg(short, long)] server: SocketAddr, /// Pre-shared key in hex format #[arg(short, long, env = "OSTP_PSK")] psk: String, /// Country code for SNI mimicry (RU, US, DE, NO, CN) #[arg(short = 'c', long, default_value = "US")] country: String, }, /// Interactive setup wizard Setup, /// Test connection to server Test { /// Server address #[arg(short, long)] server: SocketAddr, /// Pre-shared key in hex format #[arg(short, long)] psk: String, }, /// Show saved profiles Profiles, } #[derive(serde::Deserialize, serde::Serialize, Clone)] #[allow(dead_code)] struct ClientConfigFile { server: String, psk: String, country: Option, log_level: Option, } #[derive(serde::Deserialize, serde::Serialize, Default)] struct ProfilesFile { profiles: Vec, default_profile: Option, } #[derive(serde::Deserialize, serde::Serialize, Clone)] struct Profile { name: String, server: String, psk: String, country: String, } fn setup_logging(level: &str) { let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new(level)); tracing_subscriber::registry() .with(fmt::layer() .with_target(false) .with_thread_ids(false) .without_time()) .with(filter) .init(); } fn parse_psk(hex_str: &str) -> Result<[u8; 32]> { let bytes = hex::decode(hex_str.trim()) .context("Invalid hex string for PSK")?; if bytes.len() != 32 { anyhow::bail!("PSK must be exactly 32 bytes (64 hex characters), got {}", bytes.len()); } let mut psk = [0u8; 32]; psk.copy_from_slice(&bytes); Ok(psk) } fn get_config_dir() -> PathBuf { #[cfg(windows)] { std::env::var("APPDATA") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(".")) .join("ostp") } #[cfg(not(windows))] { std::env::var("HOME") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(".")) .join(".config") .join("ostp") } } fn print_banner() { println!(); println!("{}", style("╔════════════════════════════════════════════════════════╗").cyan()); println!("{}", style("║ OSTP Stealth VPN Client ║").cyan()); println!("{}", style("║ ║").cyan()); println!("{}", style("║ Invisible by Default • Contextual Mimicry ║").cyan()); println!("{}", style("╚════════════════════════════════════════════════════════╝").cyan()); println!(); } async fn interactive_setup() -> Result<()> { print_banner(); println!("{}Welcome to OSTP Client Setup Wizard\n", ROCKET); let profile_name: String = Input::new() .with_prompt("Profile name") .default("default".into()) .interact_text()?; let server: String = Input::new() .with_prompt("Server address (host:port)") .interact_text()?; let psk: String = Input::new() .with_prompt("Pre-shared key (hex)") .interact_text()?; // Validate PSK parse_psk(&psk)?; let countries = vec!["US", "RU", "DE", "NO", "CN", "Custom"]; let country_idx = Select::new() .with_prompt("Select country for SNI mimicry") .items(&countries) .default(0) .interact()?; let country = if country_idx == countries.len() - 1 { Input::new() .with_prompt("Enter custom country code") .interact_text()? } else { countries[country_idx].to_string() }; let profile = Profile { name: profile_name.clone(), server, psk, country, }; // Save profile let config_dir = get_config_dir(); std::fs::create_dir_all(&config_dir)?; let profiles_path = config_dir.join("profiles.json"); let mut profiles: ProfilesFile = if profiles_path.exists() { let content = std::fs::read_to_string(&profiles_path)?; serde_json::from_str(&content).unwrap_or_default() } else { ProfilesFile::default() }; // Remove existing profile with same name profiles.profiles.retain(|p| p.name != profile_name); profiles.profiles.push(profile); if profiles.default_profile.is_none() { profiles.default_profile = Some(profile_name.clone()); } std::fs::write(&profiles_path, serde_json::to_string_pretty(&profiles)?)?; println!(); println!("{}Profile '{}' saved to {:?}", CHECK, style(&profile_name).green(), profiles_path); if Confirm::new() .with_prompt("Connect now?") .default(true) .interact()? { let profile = profiles.profiles.iter().find(|p| p.name == profile_name).unwrap(); connect_to_server(&profile.server, &profile.psk, &profile.country).await?; } Ok(()) } async fn connect_to_server(server: &str, psk_hex: &str, country: &str) -> Result<()> { let server_addr: SocketAddr = server.parse() .context("Invalid server address")?; let psk = parse_psk(psk_hex)?; println!(); println!("{}Connecting to {}", GLOBE, style(server).yellow()); println!("{}Country mimicry: {}", LOCK, style(country).green()); println!(); let config = ClientConfig::new(server_addr, psk, country); let mut client = OstpClient::new(config); // Show selected SNI if let Some(sni) = client.suggested_sni() { println!(" SNI target: {}", style(sni).dim()); } println!(" Initiating handshake..."); match client.connect().await { Ok(_stream) => { println!(); println!("{}Connection established!", CHECK); println!(); println!("{}VPN tunnel is active. Press Ctrl+C to disconnect.", style("INFO").blue()); // Keep connection alive loop { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } } Err(e) => { println!(); println!("{}Connection failed: {}", WARN, style(&e).red()); Err(e) } } } async fn test_connection(server: SocketAddr, psk_hex: &str) -> Result<()> { let psk = parse_psk(psk_hex)?; println!(); println!("Testing connection to {}...", style(server).yellow()); let config = ClientConfig::new(server, psk, "US"); let mut client = OstpClient::new(config); let start = std::time::Instant::now(); match client.connect().await { Ok(_) => { let elapsed = start.elapsed(); println!("{}Handshake successful! ({}ms)", CHECK, elapsed.as_millis()); Ok(()) } Err(e) => { println!("{}Handshake failed: {}", WARN, e); Err(e) } } } fn show_profiles() -> Result<()> { let profiles_path = get_config_dir().join("profiles.json"); if !profiles_path.exists() { println!("No profiles found. Run 'ostp-client setup' to create one."); return Ok(()); } let content = std::fs::read_to_string(&profiles_path)?; let profiles: ProfilesFile = serde_json::from_str(&content)?; println!(); println!("{}", style("Saved Profiles:").bold()); println!(); for profile in &profiles.profiles { let is_default = profiles.default_profile.as_ref() == Some(&profile.name); let marker = if is_default { " (default)" } else { "" }; println!(" {} {}{}", style("●").green(), style(&profile.name).bold(), style(marker).dim() ); println!(" Server: {}", profile.server); println!(" Country: {}", profile.country); println!(); } Ok(()) } #[tokio::main] async fn main() -> Result<()> { // ============================================ // SECURITY CHECK - Must pass before anything else // ============================================ #[cfg(not(debug_assertions))] { if !security_init() { // Silent exit with generic error std::process::exit(1); } } let cli = Cli::parse(); match cli.command { Some(Commands::Setup) => { interactive_setup().await?; } Some(Commands::Connect { server, psk, country }) => { setup_logging(&cli.log_level); print_banner(); connect_to_server(&server.to_string(), &psk, &country).await?; } Some(Commands::Test { server, psk }) => { setup_logging(&cli.log_level); test_connection(server, &psk).await?; } Some(Commands::Profiles) => { show_profiles()?; } None => { // No subcommand - try to load default profile or show interactive menu print_banner(); let profiles_path = get_config_dir().join("profiles.json"); if profiles_path.exists() { let content = std::fs::read_to_string(&profiles_path)?; let profiles: ProfilesFile = serde_json::from_str(&content)?; if !profiles.profiles.is_empty() { let choices: Vec<&str> = profiles.profiles.iter() .map(|p| p.name.as_str()) .chain(std::iter::once("Setup new profile")) .collect(); let selection = Select::new() .with_prompt("Select profile to connect") .items(&choices) .default(0) .interact()?; if selection == choices.len() - 1 { interactive_setup().await?; } else { setup_logging(&cli.log_level); let profile = &profiles.profiles[selection]; connect_to_server(&profile.server, &profile.psk, &profile.country).await?; } } else { interactive_setup().await?; } } else { interactive_setup().await?; } } } Ok(()) } /// Security initialization - runs all protection checks fn security_init() -> bool { // Check for debuggers and VMs if !ostp_guard::init_protection() { // Don't reveal why we're failing - just "network timeout" eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT); return false; } // Check for analysis tools if ostp_guard::anti_vm::check_analysis_tools() { eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT); return false; } // Start background monitor in release builds #[cfg(not(debug_assertions))] { ostp_guard::anti_debug::start_background_monitor(); } true }