start, reverse guard, cli-frontend for server and client
This commit is contained in:
420
ostp-client/src/main.rs
Normal file
420
ostp-client/src/main.rs
Normal file
@@ -0,0 +1,420 @@
|
||||
//! OSTP Client CLI - Stealth VPN Client for Windows
|
||||
//!
|
||||
//! Usage:
|
||||
//! ostp-client connect --server 1.2.3.4:8443 --psk <hex-key>
|
||||
//! 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<Commands>,
|
||||
|
||||
/// Path to config file (JSON)
|
||||
#[arg(short, long)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// 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<String>,
|
||||
log_level: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Default)]
|
||||
struct ProfilesFile {
|
||||
profiles: Vec<Profile>,
|
||||
default_profile: Option<String>,
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
Reference in New Issue
Block a user