start, reverse guard, cli-frontend for server and client

This commit is contained in:
2026-01-01 18:54:36 +03:00
commit 5fbb32d243
30 changed files with 4700 additions and 0 deletions

26
ostp-client/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "ostp-client"
version.workspace = true
edition.workspace = true
description = "OSTP Stealth VPN Client"
[[bin]]
name = "ostp-client"
path = "src/main.rs"
[dependencies]
ostp = { path = "../ostp" }
ostp-guard = { path = "../ostp-guard" }
tokio.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
hex.workspace = true
serde.workspace = true
serde_json.workspace = true
dialoguer.workspace = true
console.workspace = true
[target.'cfg(windows)'.dependencies]
# Windows-specific deps will go here (wintun, etc.)

420
ostp-client/src/main.rs Normal file
View 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
}