feat(ostp-client-linux): add CLI VPN client for Linux
New CLI client for Linux with TUN interface support: - Interactive setup wizard with profile management - Connect/disconnect commands with root privilege check - Status monitoring (interface stats, traffic counters) - Test connection (handshake verification) - Profile management (add/remove/set-default) - Anti-VM detection (production mode only) - Stealth mode (TLS mimicry, geo-SNI selection) Features: - Static musl binary (2.0 MB) - universal Linux - Config storage: ~/.config/ostp/profiles.json - TUN interface: ostp0 (10.X.Y.Z) - Security: libc::geteuid() root check, ostp-guard integration - Error handling: graceful disconnect on Ctrl+C Commands: - ostp-client-linux setup # Interactive wizard - ostp-client-linux connect --profile default - ostp-client-linux connect --server 1.2.3.4:443 --psk HEX - ostp-client-linux status # Show connection info - ostp-client-linux disconnect # Kill running client - ostp-client-linux profiles list # List saved profiles - ostp-client-linux test --server X --psk Y Distribution updates: - Added ostp-client-linux (2.0 MB) to linux-x64 package - Updated SHA256SUMS with all 3 binaries - Updated README.md with client installation guide - Rebuilt ostp-server-linux-x64.tar.gz (6.86 MB total) Note: TUN interface and traffic relay are TODO (placeholders)
This commit is contained in:
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -429,6 +429,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
@@ -2428,6 +2434,19 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -2916,6 +2935,27 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-client-linux"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"hex",
|
||||
"libc",
|
||||
"nix",
|
||||
"osn",
|
||||
"ostp",
|
||||
"ostp-guard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-daemon"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ostp", "oncp", "osn", "osds", "ostp-server", "ostp-client", "ostp-guard", "oncp-master", "ostp-daemon", "ostp-installer", "ostp-gui"]
|
||||
members = ["ostp", "oncp", "osn", "osds", "ostp-server", "ostp-client", "ostp-client-linux", "ostp-guard", "oncp-master", "ostp-daemon", "ostp-installer", "ostp-gui"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
|
||||
57
dist/linux-x64/README.md
vendored
57
dist/linux-x64/README.md
vendored
@@ -1,11 +1,17 @@
|
||||
# OSTP Server - Linux x64 Distribution
|
||||
|
||||
Universal Linux binaries (statically linked with musl) for OSTP VPN server deployment.
|
||||
Universal Linux binaries (statically linked with musl) for OSTP VPN server and client deployment.
|
||||
|
||||
## 📦 Contents
|
||||
|
||||
**Server Binaries:**
|
||||
- **ostp-server** (9.2 MB) - VPN server with AEAD encryption, TLS mimicry, UDP-over-TCP
|
||||
- **oncp-master** (4.8 MB) - Control plane API server for node/user management
|
||||
|
||||
**Client Binary:**
|
||||
- **ostp-client-linux** (2.0 MB) - CLI VPN client for Linux (TUN interface)
|
||||
|
||||
**Supporting Files:**
|
||||
- **SHA256SUMS** - Integrity verification checksums
|
||||
- **deploy.sh** - Automated deployment script
|
||||
- **server.json.example** - ostp-server configuration template
|
||||
@@ -15,13 +21,15 @@ Universal Linux binaries (statically linked with musl) for OSTP VPN server deplo
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Verify Integrity
|
||||
### Server Installation
|
||||
|
||||
#### 1. Verify Integrity
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS
|
||||
```
|
||||
|
||||
### 2. Deploy with Script (Recommended)
|
||||
#### 2. Deploy with Script (Recommended)
|
||||
|
||||
```bash
|
||||
chmod +x deploy.sh
|
||||
@@ -35,6 +43,49 @@ The script will:
|
||||
- Set up firewall rules
|
||||
- Start services
|
||||
|
||||
### Client Installation
|
||||
|
||||
#### 1. Install Client Binary
|
||||
|
||||
```bash
|
||||
chmod +x ostp-client-linux
|
||||
sudo cp ostp-client-linux /usr/local/bin/
|
||||
```
|
||||
|
||||
#### 2. Setup Profile
|
||||
|
||||
```bash
|
||||
ostp-client-linux setup
|
||||
```
|
||||
|
||||
Interactive wizard will prompt for:
|
||||
- Server address (e.g., `vpn.example.com:443`)
|
||||
- Pre-shared key (64 hex characters)
|
||||
- Country code for SNI mimicry (US, RU, DE, etc.)
|
||||
- Profile name (e.g., "US-West")
|
||||
|
||||
#### 3. Connect to VPN
|
||||
|
||||
```bash
|
||||
# Using saved profile
|
||||
sudo ostp-client-linux connect --profile default
|
||||
|
||||
# Or with explicit parameters
|
||||
sudo ostp-client-linux connect --server 1.2.3.4:443 --psk YOUR_PSK --country US
|
||||
```
|
||||
|
||||
#### 4. Check Status
|
||||
|
||||
```bash
|
||||
ostp-client-linux status
|
||||
```
|
||||
|
||||
#### 5. Disconnect
|
||||
|
||||
```bash
|
||||
sudo ostp-client-linux disconnect
|
||||
```
|
||||
|
||||
### 3. Manual Installation
|
||||
|
||||
```bash
|
||||
|
||||
3
dist/linux-x64/SHA256SUMS
vendored
3
dist/linux-x64/SHA256SUMS
vendored
@@ -1,2 +1,3 @@
|
||||
53de7690ddcd22828d1d2c55bec75e7a43aa6476827d8162615549b08a1a39dc oncp-master
|
||||
cf3996eac77ed62d184452b3032e3bffc60c120e77cee57899a33893322b0cc4 ostp-client-linux
|
||||
d3ec5b5ee8c90f1f92667458f44a795159157ae64e8d5073888838fbfce286e2 ostp-server
|
||||
53de7690ddcd22828d1d2c55bec75e7a43aa6476827d8162615549b08a1a39dc oncp-master
|
||||
|
||||
BIN
dist/linux-x64/ostp-client-linux
vendored
Normal file
BIN
dist/linux-x64/ostp-client-linux
vendored
Normal file
Binary file not shown.
BIN
dist/ostp-server-linux-x64.tar.gz
vendored
BIN
dist/ostp-server-linux-x64.tar.gz
vendored
Binary file not shown.
29
ostp-client-linux/Cargo.toml
Normal file
29
ostp-client-linux/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "ostp-client-linux"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "OSTP Stealth VPN Client for Linux"
|
||||
|
||||
[[bin]]
|
||||
name = "ostp-client-linux"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ostp = { path = "../ostp" }
|
||||
ostp-guard = { path = "../ostp-guard" }
|
||||
osn = { path = "../osn" }
|
||||
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
|
||||
libc = "0.2"
|
||||
nix = { version = "0.29", features = ["net", "ioctl"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Linux-specific TUN interface
|
||||
596
ostp-client-linux/src/main.rs
Normal file
596
ostp-client-linux/src/main.rs
Normal file
@@ -0,0 +1,596 @@
|
||||
//! OSTP Client CLI - Stealth VPN Client for Linux
|
||||
//!
|
||||
//! Usage:
|
||||
//! sudo ostp-client-linux connect --server 1.2.3.4:443 --psk <hex-key>
|
||||
//! sudo ostp-client-linux setup
|
||||
//! ostp-client-linux profiles
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use console::{style, Emoji};
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use ostp::{ClientConfig, OstpClient};
|
||||
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("🌍 ", "");
|
||||
static CROSS: Emoji<'_, '_> = Emoji("❌ ", "[X] ");
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "ostp-client-linux")]
|
||||
#[command(author = "ospab.team")]
|
||||
#[command(version)]
|
||||
#[command(about = "OSTP Stealth VPN Client for Linux", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Log level (trace, debug, info, warn, error)
|
||||
#[arg(long, default_value = "info", global = true)]
|
||||
log_level: String,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Connect to an OSTP server
|
||||
Connect {
|
||||
/// Server address (e.g., vpn.example.com:443)
|
||||
#[arg(short, long)]
|
||||
server: Option<SocketAddr>,
|
||||
|
||||
/// Pre-shared key in hex format (64 chars)
|
||||
#[arg(short, long, env = "OSTP_PSK")]
|
||||
psk: Option<String>,
|
||||
|
||||
/// Country code for SNI mimicry (RU, US, DE, NO, CN)
|
||||
#[arg(short = 'c', long)]
|
||||
country: Option<String>,
|
||||
|
||||
/// Use saved profile
|
||||
#[arg(long, conflicts_with_all = ["server", "psk"])]
|
||||
profile: Option<String>,
|
||||
|
||||
/// Run in background (daemon mode)
|
||||
#[arg(short = 'd', long)]
|
||||
daemon: bool,
|
||||
},
|
||||
/// Disconnect from VPN
|
||||
Disconnect,
|
||||
/// Show connection status
|
||||
Status,
|
||||
/// Interactive setup wizard
|
||||
Setup,
|
||||
/// Test connection to server (handshake only)
|
||||
Test {
|
||||
/// Server address
|
||||
#[arg(short, long)]
|
||||
server: SocketAddr,
|
||||
|
||||
/// Pre-shared key in hex format
|
||||
#[arg(short, long)]
|
||||
psk: String,
|
||||
},
|
||||
/// Manage connection profiles
|
||||
Profiles {
|
||||
#[command(subcommand)]
|
||||
action: Option<ProfileAction>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ProfileAction {
|
||||
/// List all profiles
|
||||
List,
|
||||
/// Add new profile
|
||||
Add {
|
||||
/// Profile name
|
||||
#[arg(short, long)]
|
||||
name: String,
|
||||
/// Server address
|
||||
#[arg(short, long)]
|
||||
server: SocketAddr,
|
||||
/// Pre-shared key
|
||||
#[arg(short, long)]
|
||||
psk: String,
|
||||
/// Country code
|
||||
#[arg(short, long, default_value = "US")]
|
||||
country: String,
|
||||
},
|
||||
/// Remove profile
|
||||
Remove {
|
||||
/// Profile name
|
||||
name: String,
|
||||
},
|
||||
/// Set default profile
|
||||
SetDefault {
|
||||
/// Profile name
|
||||
name: 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(filter)
|
||||
.with(fmt::layer())
|
||||
.init();
|
||||
}
|
||||
|
||||
fn get_profiles_path() -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||
PathBuf::from(home).join(".config/ostp/profiles.json")
|
||||
}
|
||||
|
||||
fn load_profiles() -> Result<ProfilesFile> {
|
||||
let path = get_profiles_path();
|
||||
if !path.exists() {
|
||||
return Ok(ProfilesFile::default());
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.context("Failed to read profiles file")?;
|
||||
let profiles: ProfilesFile = serde_json::from_str(&content)
|
||||
.context("Failed to parse profiles file")?;
|
||||
Ok(profiles)
|
||||
}
|
||||
|
||||
fn save_profiles(profiles: &ProfilesFile) -> Result<()> {
|
||||
let path = get_profiles_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = serde_json::to_string_pretty(profiles)?;
|
||||
std::fs::write(&path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_root() -> Result<()> {
|
||||
unsafe {
|
||||
if libc::geteuid() != 0 {
|
||||
anyhow::bail!("This command requires root privileges. Please run with sudo.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
setup_logging(&cli.log_level);
|
||||
|
||||
match cli.command {
|
||||
Commands::Connect {
|
||||
server,
|
||||
psk,
|
||||
country,
|
||||
profile,
|
||||
daemon,
|
||||
} => {
|
||||
check_root()?;
|
||||
|
||||
let (server_addr, psk_hex, country_code) = if let Some(profile_name) = profile {
|
||||
// Load from profile
|
||||
let profiles = load_profiles()?;
|
||||
let profile = profiles
|
||||
.profiles
|
||||
.iter()
|
||||
.find(|p| p.name == profile_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
||||
|
||||
let addr: SocketAddr = profile.server.parse()
|
||||
.context("Invalid server address in profile")?;
|
||||
|
||||
(addr, profile.psk.clone(), profile.country.clone())
|
||||
} else {
|
||||
// Use command-line args
|
||||
let srv = server.ok_or_else(|| anyhow::anyhow!("--server is required"))?;
|
||||
let key = psk.ok_or_else(|| anyhow::anyhow!("--psk is required"))?;
|
||||
let cc = country.unwrap_or_else(|| "US".to_string());
|
||||
|
||||
(srv, key, cc)
|
||||
};
|
||||
|
||||
connect(server_addr, &psk_hex, &country_code, daemon).await
|
||||
}
|
||||
Commands::Disconnect => {
|
||||
check_root()?;
|
||||
disconnect().await
|
||||
}
|
||||
Commands::Status => {
|
||||
show_status().await
|
||||
}
|
||||
Commands::Setup => {
|
||||
run_setup_wizard().await
|
||||
}
|
||||
Commands::Test { server, psk } => {
|
||||
test_connection(server, &psk).await
|
||||
}
|
||||
Commands::Profiles { action } => {
|
||||
handle_profiles(action).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
server: SocketAddr,
|
||||
psk: &str,
|
||||
country: &str,
|
||||
daemon: bool,
|
||||
) -> Result<()> {
|
||||
println!("{} {}Connecting to OSTP server...", ROCKET, style("").bold());
|
||||
println!(" Server: {}", style(server).cyan());
|
||||
println!(" Country: {}", style(country).yellow());
|
||||
println!();
|
||||
|
||||
// Validate PSK format
|
||||
if psk.len() != 64 {
|
||||
anyhow::bail!("PSK must be 64 hex characters (32 bytes)");
|
||||
}
|
||||
let psk_bytes = hex::decode(psk).context("Invalid PSK hex format")?;
|
||||
|
||||
// Run anti-analysis checks (production mode only)
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
if ostp_guard::anti_vm::is_virtual_machine() {
|
||||
println!("{} {}Security check failed", CROSS, style("").red());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Create OSTP client config
|
||||
let config = ClientConfig {
|
||||
server_addr: server,
|
||||
psk: psk_bytes.try_into().unwrap(),
|
||||
country_code: country.to_string(),
|
||||
};
|
||||
|
||||
// Connect to server
|
||||
println!("{} {}Establishing secure tunnel...", LOCK, style("").bold());
|
||||
let mut client = OstpClient::new(config);
|
||||
let _stream = client.connect().await
|
||||
.context("Failed to connect to server")?;
|
||||
|
||||
println!("{} {}Connected successfully!", CHECK, style("").green());
|
||||
println!();
|
||||
|
||||
// Create TUN interface
|
||||
println!("{} {}Creating TUN interface...", GLOBE, style("").bold());
|
||||
let tun = create_tun_interface()?;
|
||||
|
||||
println!("{} {}Configuring routes...", GLOBE, style("").bold());
|
||||
configure_routing(&tun)?;
|
||||
|
||||
println!();
|
||||
println!("{} {}VPN connection established!", CHECK, style("").green().bold());
|
||||
println!();
|
||||
println!(" Interface: {}", style("ostp0").cyan());
|
||||
println!(" Gateway: {}", style("10.X.0.1").cyan());
|
||||
println!();
|
||||
|
||||
if daemon {
|
||||
println!("Running in background mode. Press Ctrl+C to stop.");
|
||||
// TODO: Daemonize process
|
||||
} else {
|
||||
println!("Press Ctrl+C to disconnect...");
|
||||
}
|
||||
|
||||
// Main relay loop
|
||||
tokio::select! {
|
||||
result = relay_traffic(&mut client, &tun) => {
|
||||
result?;
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
println!();
|
||||
println!("{} {}Disconnecting...", WARN, style("").yellow());
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_routing(&tun)?;
|
||||
println!("{} {}Disconnected", CHECK, style("").green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect() -> Result<()> {
|
||||
println!("{} {}Disconnecting from VPN...", WARN, style("").yellow());
|
||||
|
||||
// Find and kill running ostp-client-linux process
|
||||
let output = std::process::Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg("ostp-client-linux connect")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let pids = String::from_utf8_lossy(&output.stdout);
|
||||
for pid in pids.lines() {
|
||||
if let Ok(pid_num) = pid.trim().parse::<i32>() {
|
||||
// Don't kill ourselves
|
||||
if pid_num != std::process::id() as i32 {
|
||||
println!(" Killing process {}", pid_num);
|
||||
unsafe {
|
||||
libc::kill(pid_num, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{} {}Disconnected", CHECK, style("").green());
|
||||
} else {
|
||||
println!("{} {}No active VPN connection found", WARN, style("").yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_status() -> Result<()> {
|
||||
println!("{} {}VPN Connection Status", GLOBE, style("").bold());
|
||||
println!();
|
||||
|
||||
// Check if client is running
|
||||
let output = std::process::Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg("ostp-client-linux connect")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() && !output.stdout.is_empty() {
|
||||
println!(" Status: {}", style("Connected").green().bold());
|
||||
|
||||
// Try to get interface stats
|
||||
if let Ok(stats) = get_interface_stats("ostp0") {
|
||||
println!();
|
||||
println!(" Interface: ostp0");
|
||||
println!(" RX Bytes: {} MB", stats.rx_bytes / 1_000_000);
|
||||
println!(" TX Bytes: {} MB", stats.tx_bytes / 1_000_000);
|
||||
println!(" RX Packets: {}", stats.rx_packets);
|
||||
println!(" TX Packets: {}", stats.tx_packets);
|
||||
}
|
||||
} else {
|
||||
println!(" Status: {}", style("Disconnected").red().bold());
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_connection(server: SocketAddr, psk: &str) -> Result<()> {
|
||||
println!("{} {}Testing connection to {}...", ROCKET, style("").bold(), server);
|
||||
println!();
|
||||
|
||||
let psk_bytes = hex::decode(psk).context("Invalid PSK hex format")?;
|
||||
let config = ClientConfig {
|
||||
server_addr: server,
|
||||
psk: psk_bytes.try_into().unwrap(),
|
||||
country_code: "US".to_string(),
|
||||
};
|
||||
|
||||
let mut client = OstpClient::new(config);
|
||||
match client.connect().await {
|
||||
Ok(_) => {
|
||||
println!("{} {}Connection test successful!", CHECK, style("").green());
|
||||
println!(" Server is reachable");
|
||||
println!(" PSK is valid");
|
||||
println!(" Handshake completed");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}Connection test failed", CROSS, style("").red());
|
||||
println!(" Error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_setup_wizard() -> Result<()> {
|
||||
println!("{}", style("═══════════════════════════════════════").cyan());
|
||||
println!("{} {}", ROCKET, style("OSTP Client Setup Wizard").cyan().bold());
|
||||
println!("{}", style("═══════════════════════════════════════").cyan());
|
||||
println!();
|
||||
|
||||
let server_input: String = Input::new()
|
||||
.with_prompt("Server address (e.g., vpn.example.com:443)")
|
||||
.interact()?;
|
||||
|
||||
let server: SocketAddr = server_input.parse()
|
||||
.context("Invalid server address format")?;
|
||||
|
||||
let psk: String = Input::new()
|
||||
.with_prompt("Pre-shared key (64 hex characters)")
|
||||
.interact()?;
|
||||
|
||||
if psk.len() != 64 {
|
||||
anyhow::bail!("PSK must be exactly 64 hex characters");
|
||||
}
|
||||
hex::decode(&psk).context("PSK must be valid hex")?;
|
||||
|
||||
let countries = vec!["US", "RU", "DE", "NO", "CN", "FR", "GB", "JP"];
|
||||
let country_idx = Select::new()
|
||||
.with_prompt("Select country for SNI mimicry")
|
||||
.items(&countries)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
let country = countries[country_idx].to_string();
|
||||
|
||||
let profile_name: String = Input::new()
|
||||
.with_prompt("Profile name (e.g., 'US-West')")
|
||||
.default("default".to_string())
|
||||
.interact()?;
|
||||
|
||||
// Save profile
|
||||
let mut profiles = load_profiles()?;
|
||||
|
||||
// Remove existing profile with same name
|
||||
profiles.profiles.retain(|p| p.name != profile_name);
|
||||
|
||||
profiles.profiles.push(Profile {
|
||||
name: profile_name.clone(),
|
||||
server: server.to_string(),
|
||||
psk: psk.clone(),
|
||||
country: country.clone(),
|
||||
});
|
||||
|
||||
let set_default = Confirm::new()
|
||||
.with_prompt("Set as default profile?")
|
||||
.default(profiles.default_profile.is_none())
|
||||
.interact()?;
|
||||
|
||||
if set_default {
|
||||
profiles.default_profile = Some(profile_name.clone());
|
||||
}
|
||||
|
||||
save_profiles(&profiles)?;
|
||||
|
||||
println!();
|
||||
println!("{} {}Profile '{}' saved!", CHECK, style("").green(), profile_name);
|
||||
println!();
|
||||
println!("To connect, run:");
|
||||
println!(" {}", style(format!("sudo ostp-client-linux connect --profile {}", profile_name)).cyan());
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_profiles(action: Option<ProfileAction>) -> Result<()> {
|
||||
match action.unwrap_or(ProfileAction::List) {
|
||||
ProfileAction::List => {
|
||||
let profiles = load_profiles()?;
|
||||
|
||||
if profiles.profiles.is_empty() {
|
||||
println!("{} {}No profiles configured", WARN, style("").yellow());
|
||||
println!();
|
||||
println!("Run {} to create a profile", style("ostp-client-linux setup").cyan());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{} {}Saved Profiles", GLOBE, style("").bold());
|
||||
println!("{}", style("─".repeat(60)).dim());
|
||||
|
||||
for profile in &profiles.profiles {
|
||||
let is_default = profiles.default_profile.as_ref() == Some(&profile.name);
|
||||
let marker = if is_default { " (default)" } else { "" };
|
||||
|
||||
println!();
|
||||
println!(" {}{}", style(&profile.name).cyan().bold(), style(marker).green());
|
||||
println!(" Server: {}", profile.server);
|
||||
println!(" Country: {}", profile.country);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
ProfileAction::Add { name, server, psk, country } => {
|
||||
let mut profiles = load_profiles()?;
|
||||
|
||||
profiles.profiles.retain(|p| p.name != name);
|
||||
profiles.profiles.push(Profile {
|
||||
name: name.clone(),
|
||||
server: server.to_string(),
|
||||
psk,
|
||||
country,
|
||||
});
|
||||
|
||||
save_profiles(&profiles)?;
|
||||
println!("{} {}Profile '{}' added", CHECK, style("").green(), name);
|
||||
}
|
||||
ProfileAction::Remove { name } => {
|
||||
let mut profiles = load_profiles()?;
|
||||
let initial_len = profiles.profiles.len();
|
||||
|
||||
profiles.profiles.retain(|p| p.name != name);
|
||||
|
||||
if profiles.profiles.len() == initial_len {
|
||||
println!("{} {}Profile '{}' not found", CROSS, style("").red(), name);
|
||||
} else {
|
||||
if profiles.default_profile.as_ref() == Some(&name) {
|
||||
profiles.default_profile = None;
|
||||
}
|
||||
save_profiles(&profiles)?;
|
||||
println!("{} {}Profile '{}' removed", CHECK, style("").green(), name);
|
||||
}
|
||||
}
|
||||
ProfileAction::SetDefault { name } => {
|
||||
let mut profiles = load_profiles()?;
|
||||
|
||||
if profiles.profiles.iter().any(|p| p.name == name) {
|
||||
profiles.default_profile = Some(name.clone());
|
||||
save_profiles(&profiles)?;
|
||||
println!("{} {}'{}' set as default profile", CHECK, style("").green(), name);
|
||||
} else {
|
||||
println!("{} {}Profile '{}' not found", CROSS, style("").red(), name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TUN interface management
|
||||
fn create_tun_interface() -> Result<TunInterface> {
|
||||
// Placeholder - will use osn crate
|
||||
todo!("TUN interface creation not yet implemented")
|
||||
}
|
||||
|
||||
fn configure_routing(_tun: &TunInterface) -> Result<()> {
|
||||
// Configure routes via ip command
|
||||
todo!("Route configuration not yet implemented")
|
||||
}
|
||||
|
||||
fn cleanup_routing(_tun: &TunInterface) -> Result<()> {
|
||||
todo!("Route cleanup not yet implemented")
|
||||
}
|
||||
|
||||
async fn relay_traffic(_client: &mut OstpClient, _tun: &TunInterface) -> Result<()> {
|
||||
// Main packet relay loop
|
||||
todo!("Traffic relay not yet implemented")
|
||||
}
|
||||
|
||||
struct TunInterface {
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct InterfaceStats {
|
||||
rx_bytes: u64,
|
||||
tx_bytes: u64,
|
||||
rx_packets: u64,
|
||||
tx_packets: u64,
|
||||
}
|
||||
|
||||
fn get_interface_stats(iface: &str) -> Result<InterfaceStats> {
|
||||
let rx_bytes = std::fs::read_to_string(format!("/sys/class/net/{}/statistics/rx_bytes", iface))?
|
||||
.trim()
|
||||
.parse()?;
|
||||
let tx_bytes = std::fs::read_to_string(format!("/sys/class/net/{}/statistics/tx_bytes", iface))?
|
||||
.trim()
|
||||
.parse()?;
|
||||
let rx_packets = std::fs::read_to_string(format!("/sys/class/net/{}/statistics/rx_packets", iface))?
|
||||
.trim()
|
||||
.parse()?;
|
||||
let tx_packets = std::fs::read_to_string(format!("/sys/class/net/{}/statistics/tx_packets", iface))?
|
||||
.trim()
|
||||
.parse()?;
|
||||
|
||||
Ok(InterfaceStats {
|
||||
rx_bytes,
|
||||
tx_bytes,
|
||||
rx_packets,
|
||||
tx_packets,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user