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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -2428,6 +2434,19 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "nodrop"
|
name = "nodrop"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@@ -2916,6 +2935,27 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"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]]
|
[[package]]
|
||||||
name = "ostp-daemon"
|
name = "ostp-daemon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
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]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
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
|
# 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
|
## 📦 Contents
|
||||||
|
|
||||||
|
**Server Binaries:**
|
||||||
- **ostp-server** (9.2 MB) - VPN server with AEAD encryption, TLS mimicry, UDP-over-TCP
|
- **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
|
- **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
|
- **SHA256SUMS** - Integrity verification checksums
|
||||||
- **deploy.sh** - Automated deployment script
|
- **deploy.sh** - Automated deployment script
|
||||||
- **server.json.example** - ostp-server configuration template
|
- **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
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### 1. Verify Integrity
|
### Server Installation
|
||||||
|
|
||||||
|
#### 1. Verify Integrity
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum -c SHA256SUMS
|
sha256sum -c SHA256SUMS
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Deploy with Script (Recommended)
|
#### 2. Deploy with Script (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x deploy.sh
|
chmod +x deploy.sh
|
||||||
@@ -35,6 +43,49 @@ The script will:
|
|||||||
- Set up firewall rules
|
- Set up firewall rules
|
||||||
- Start services
|
- 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
|
### 3. Manual Installation
|
||||||
|
|
||||||
```bash
|
```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
|
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