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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/.cargo
/.github
/prompt.md
/target

1614
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

52
Cargo.toml Normal file
View File

@@ -0,0 +1,52 @@
[workspace]
resolver = "2"
members = ["ostp", "oncp", "osn", "osds", "ostp-server", "ostp-client", "ostp-guard"]
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["Ospab Team"]
license = "Proprietary"
# ============================================================================
# HARDENED RELEASE PROFILE - Anti-Reverse Engineering
# ============================================================================
[profile.release]
lto = true # Link-Time Optimization - merges all code, removes boundaries
codegen-units = 1 # Single codegen unit - better optimization, harder to analyze
panic = "abort" # No unwinding info - smaller binary, no stack traces
strip = "symbols" # Remove ALL symbols - no function names in binary
opt-level = 3 # Maximum optimization
debug = false # No debug info
debug-assertions = false # No debug assertions
overflow-checks = false # No overflow checks (performance + less obvious control flow)
incremental = false # Reproducible builds
# Distribution build - even more aggressive
[profile.dist]
inherits = "release"
lto = "fat" # Full LTO across all crates
strip = "debuginfo" # Alternative stripping
[workspace.dependencies]
tokio = { version = "1.40", features = ["full"] }
ring = "0.17"
chacha20poly1305 = "0.10"
x25519-dalek = { version = "2.0", features = ["static_secrets"] }
bytes = "1.7"
rusqlite = { version = "0.32", features = ["bundled"] }
anyhow = "1.0"
thiserror = "2.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.10", features = ["v4", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hmac = "0.12"
sha2 = "0.10"
rand = "0.8"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5", features = ["derive", "env"] }
hex = "0.4"
dialoguer = "0.11"
console = "0.15"

16
oncp/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "oncp"
version.workspace = true
edition.workspace = true
[dependencies]
tokio.workspace = true
rusqlite.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
uuid.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
ostp = { path = "../ostp" }

163
oncp/src/billing.rs Normal file
View File

@@ -0,0 +1,163 @@
//! User billing and quota management
use chrono::{DateTime, Utc};
use std::sync::Mutex;
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum BillingError {
#[error("user not found: {0}")]
UserNotFound(Uuid),
#[error("subscription expired")]
Expired,
#[error("bandwidth quota exceeded")]
QuotaExceeded,
#[error("database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("lock error")]
LockError,
}
/// User account with billing info
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
pub uuid: Uuid,
pub bandwidth_quota: u64, // bytes allowed
pub bandwidth_used: u64, // bytes used
pub expires_at: DateTime<Utc>, // subscription expiry
pub active: bool,
}
impl User {
pub fn new(quota_gb: u64, valid_days: i64) -> Self {
Self {
uuid: Uuid::new_v4(),
bandwidth_quota: quota_gb * 1024 * 1024 * 1024,
bandwidth_used: 0,
expires_at: Utc::now() + chrono::Duration::days(valid_days),
active: true,
}
}
pub fn is_valid(&self) -> bool {
self.active && Utc::now() < self.expires_at && self.bandwidth_used < self.bandwidth_quota
}
pub fn remaining_quota(&self) -> u64 {
self.bandwidth_quota.saturating_sub(self.bandwidth_used)
}
}
/// Abstract user registry trait
pub trait UserRegistry: Send + Sync {
fn get_user(&self, uuid: &Uuid) -> Result<Option<User>, BillingError>;
fn create_user(&self, user: &User) -> Result<(), BillingError>;
fn update_bandwidth(&self, uuid: &Uuid, bytes: u64) -> Result<(), BillingError>;
fn validate_user(&self, uuid: &Uuid) -> Result<bool, BillingError>;
}
/// SQLite implementation (thread-safe via Mutex)
pub struct SqliteRegistry {
conn: Mutex<rusqlite::Connection>,
}
impl SqliteRegistry {
pub fn new(path: &str) -> Result<Self, BillingError> {
let conn = rusqlite::Connection::open(path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
uuid TEXT PRIMARY KEY,
bandwidth_quota INTEGER NOT NULL,
bandwidth_used INTEGER NOT NULL DEFAULT 0,
expires_at TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1
)",
[],
)?;
Ok(Self { conn: Mutex::new(conn) })
}
pub fn in_memory() -> Result<Self, BillingError> {
Self::new(":memory:")
}
}
impl UserRegistry for SqliteRegistry {
fn get_user(&self, uuid: &Uuid) -> Result<Option<User>, BillingError> {
let conn = self.conn.lock().map_err(|_| BillingError::LockError)?;
let mut stmt = conn
.prepare("SELECT uuid, bandwidth_quota, bandwidth_used, expires_at, active FROM users WHERE uuid = ?")?;
let mut rows = stmt.query([uuid.to_string()])?;
if let Some(row) = rows.next()? {
let uuid_str: String = row.get(0)?;
let expires_str: String = row.get(3)?;
Ok(Some(User {
uuid: Uuid::parse_str(&uuid_str).unwrap(),
bandwidth_quota: row.get(1)?,
bandwidth_used: row.get(2)?,
expires_at: DateTime::parse_from_rfc3339(&expires_str)
.unwrap()
.with_timezone(&Utc),
active: row.get::<_, i32>(4)? == 1,
}))
} else {
Ok(None)
}
}
fn create_user(&self, user: &User) -> Result<(), BillingError> {
let conn = self.conn.lock().map_err(|_| BillingError::LockError)?;
conn.execute(
"INSERT INTO users (uuid, bandwidth_quota, bandwidth_used, expires_at, active) VALUES (?, ?, ?, ?, ?)",
(
user.uuid.to_string(),
user.bandwidth_quota,
user.bandwidth_used,
user.expires_at.to_rfc3339(),
if user.active { 1 } else { 0 },
),
)?;
Ok(())
}
fn update_bandwidth(&self, uuid: &Uuid, bytes: u64) -> Result<(), BillingError> {
let conn = self.conn.lock().map_err(|_| BillingError::LockError)?;
let updated = conn.execute(
"UPDATE users SET bandwidth_used = bandwidth_used + ? WHERE uuid = ?",
(bytes, uuid.to_string()),
)?;
if updated == 0 {
return Err(BillingError::UserNotFound(*uuid));
}
Ok(())
}
fn validate_user(&self, uuid: &Uuid) -> Result<bool, BillingError> {
match self.get_user(uuid)? {
Some(user) => Ok(user.is_valid()),
None => Ok(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_lifecycle() {
let reg = SqliteRegistry::in_memory().unwrap();
let user = User::new(100, 30); // 100GB, 30 days
let uuid = user.uuid;
reg.create_user(&user).unwrap();
assert!(reg.validate_user(&uuid).unwrap());
reg.update_bandwidth(&uuid, 1024).unwrap();
let updated = reg.get_user(&uuid).unwrap().unwrap();
assert_eq!(updated.bandwidth_used, 1024);
}
}

5
oncp/src/lib.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod billing;
pub mod session;
pub use billing::{BillingError, SqliteRegistry, User, UserRegistry};
pub use session::{Session, SessionManager};

109
oncp/src/session.rs Normal file
View File

@@ -0,0 +1,109 @@
//! Session management and heartbeat
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use uuid::Uuid;
/// Session state
#[derive(Debug, Clone)]
pub struct Session {
pub user_id: Uuid,
pub session_id: Uuid,
pub created_at: Instant,
pub last_heartbeat: Instant,
pub bytes_tx: u64,
pub bytes_rx: u64,
pub sni: String,
}
impl Session {
pub fn new(user_id: Uuid, sni: String) -> Self {
let now = Instant::now();
Self {
user_id,
session_id: Uuid::new_v4(),
created_at: now,
last_heartbeat: now,
bytes_tx: 0,
bytes_rx: 0,
sni,
}
}
pub fn heartbeat(&mut self) {
self.last_heartbeat = Instant::now();
}
pub fn is_alive(&self, timeout: Duration) -> bool {
self.last_heartbeat.elapsed() < timeout
}
}
/// Session manager
pub struct SessionManager {
sessions: Arc<RwLock<HashMap<Uuid, Session>>>,
heartbeat_timeout: Duration,
}
impl SessionManager {
pub fn new(heartbeat_timeout_secs: u64) -> Self {
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
heartbeat_timeout: Duration::from_secs(heartbeat_timeout_secs),
}
}
pub async fn create_session(&self, user_id: Uuid, sni: String) -> Uuid {
let session = Session::new(user_id, sni);
let session_id = session.session_id;
self.sessions.write().await.insert(session_id, session);
session_id
}
pub async fn heartbeat(&self, session_id: &Uuid) -> bool {
if let Some(session) = self.sessions.write().await.get_mut(session_id) {
session.heartbeat();
true
} else {
false
}
}
pub async fn get_session(&self, session_id: &Uuid) -> Option<Session> {
self.sessions.read().await.get(session_id).cloned()
}
pub async fn remove_session(&self, session_id: &Uuid) -> Option<Session> {
self.sessions.write().await.remove(session_id)
}
pub async fn cleanup_stale(&self) -> Vec<Uuid> {
let mut sessions = self.sessions.write().await;
let timeout = self.heartbeat_timeout;
let stale: Vec<Uuid> = sessions
.iter()
.filter(|(_, s)| !s.is_alive(timeout))
.map(|(id, _)| *id)
.collect();
for id in &stale {
sessions.remove(id);
}
stale
}
pub async fn update_traffic(&self, session_id: &Uuid, tx: u64, rx: u64) {
if let Some(session) = self.sessions.write().await.get_mut(session_id) {
session.bytes_tx += tx;
session.bytes_rx += rx;
}
}
}
impl Default for SessionManager {
fn default() -> Self {
Self::new(60)
}
}

13
osds/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "osds"
version.workspace = true
edition.workspace = true
[dependencies]
tokio.workspace = true
bytes.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
rand.workspace = true
ostp = { path = "../ostp" }

139
osds/src/dns.rs Normal file
View File

@@ -0,0 +1,139 @@
//! Stealth DNS resolver - DoH/DoT with anti-hijacking
use bytes::{BufMut, BytesMut};
use std::net::SocketAddr;
use thiserror::Error;
use tokio::net::UdpSocket;
#[derive(Error, Debug)]
pub enum DnsError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid query")]
InvalidQuery,
#[error("upstream timeout")]
Timeout,
#[error("hijacking detected")]
HijackDetected,
}
/// DNS query types
#[derive(Debug, Clone, Copy)]
pub enum QueryType {
A = 1,
AAAA = 28,
CNAME = 5,
TXT = 16,
}
/// Minimal DNS query builder
pub fn build_dns_query(domain: &str, qtype: QueryType) -> BytesMut {
let mut buf = BytesMut::with_capacity(512);
// Transaction ID (random)
let txid: u16 = rand::random();
buf.put_u16(txid);
// Flags: standard query, recursion desired
buf.put_u16(0x0100);
// Questions: 1, Answers: 0, Authority: 0, Additional: 0
buf.put_u16(1);
buf.put_u16(0);
buf.put_u16(0);
buf.put_u16(0);
// QNAME
for label in domain.split('.') {
buf.put_u8(label.len() as u8);
buf.put_slice(label.as_bytes());
}
buf.put_u8(0); // null terminator
// QTYPE and QCLASS (IN)
buf.put_u16(qtype as u16);
buf.put_u16(1); // IN class
buf
}
/// DNS forwarder that tunnels queries through OSTP
pub struct StealthDnsForwarder {
listen_addr: SocketAddr,
/// Upstream resolver (will be tunneled through OSTP)
upstream: SocketAddr,
}
impl StealthDnsForwarder {
pub fn new(listen: SocketAddr, upstream: SocketAddr) -> Self {
Self {
listen_addr: listen,
upstream,
}
}
/// Start DNS listener (intercepts local queries)
pub async fn run(&self) -> Result<(), DnsError> {
let socket = UdpSocket::bind(self.listen_addr).await?;
tracing::info!("DNS forwarder listening on {}", self.listen_addr);
let mut buf = [0u8; 512];
loop {
let (len, src) = socket.recv_from(&mut buf).await?;
let query = &buf[..len];
// TODO: Encrypt and forward through OSTP tunnel instead of direct
// For now, direct forward (to be replaced with tunnel)
match self.forward_query(query).await {
Ok(response) => {
let _ = socket.send_to(&response, src).await;
}
Err(e) => {
tracing::warn!("DNS forward failed: {}", e);
}
}
}
}
async fn forward_query(&self, query: &[u8]) -> Result<Vec<u8>, DnsError> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.send_to(query, self.upstream).await?;
let mut response = vec![0u8; 512];
let timeout = tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.recv_from(&mut response),
)
.await;
match timeout {
Ok(Ok((len, _))) => {
response.truncate(len);
Ok(response)
}
Ok(Err(e)) => Err(DnsError::Io(e)),
Err(_) => Err(DnsError::Timeout),
}
}
}
/// Anti-hijacking: verify response matches known-good resolver fingerprint
pub fn detect_hijack(response: &[u8], expected_patterns: &[[u8; 4]]) -> bool {
// Check if response contains known hijack IPs (ISP redirect pages, etc.)
// Simplified: check for common hijack patterns
if response.len() < 12 {
return true; // Suspicious short response
}
// Check for NXDOMAIN being rewritten (common hijack)
let rcode = response[3] & 0x0F;
if rcode == 0 {
// NOERROR - check if IP is in known hijack list
for pattern in expected_patterns {
if response.windows(4).any(|w| w == pattern) {
return true;
}
}
}
false
}

3
osds/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod dns;
pub use dns::{build_dns_query, detect_hijack, DnsError, QueryType, StealthDnsForwarder};

12
osn/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "osn"
version.workspace = true
edition.workspace = true
[dependencies]
tokio.workspace = true
bytes.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
async-trait = "0.1"

3
osn/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod tun;
pub use tun::{DummyTun, Router, TunConfig, TunDevice, TunError};

95
osn/src/tun.rs Normal file
View File

@@ -0,0 +1,95 @@
//! Virtual network interface (TUN/TAP) abstraction
use bytes::Bytes;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TunError {
#[error("failed to create interface: {0}")]
Create(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("interface not ready")]
NotReady,
}
/// TUN device configuration
#[derive(Debug, Clone)]
pub struct TunConfig {
pub name: String,
pub address: [u8; 4],
pub netmask: [u8; 4],
pub mtu: u16,
}
impl Default for TunConfig {
fn default() -> Self {
Self {
name: "ostp0".into(),
address: [10, 0, 0, 1],
netmask: [255, 255, 255, 0],
mtu: 1400,
}
}
}
/// Abstract TUN device trait
#[async_trait::async_trait]
pub trait TunDevice: Send + Sync {
async fn read(&self) -> Result<Bytes, TunError>;
async fn write(&self, data: &[u8]) -> Result<usize, TunError>;
fn mtu(&self) -> u16;
}
/// Placeholder TUN implementation (platform-specific impl needed)
pub struct DummyTun {
config: TunConfig,
}
impl DummyTun {
pub fn new(config: TunConfig) -> Self {
Self { config }
}
}
#[async_trait::async_trait]
impl TunDevice for DummyTun {
async fn read(&self) -> Result<Bytes, TunError> {
// Platform-specific: use wintun on Windows, tun-tap on Linux
Err(TunError::NotReady)
}
async fn write(&self, _data: &[u8]) -> Result<usize, TunError> {
Err(TunError::NotReady)
}
fn mtu(&self) -> u16 {
self.config.mtu
}
}
/// IP packet routing helper
pub struct Router {
default_gateway: [u8; 4],
}
impl Router {
pub fn new(gateway: [u8; 4]) -> Self {
Self {
default_gateway: gateway,
}
}
/// Check if IP should be tunneled
pub fn should_tunnel(&self, dest_ip: &[u8; 4]) -> bool {
// Don't tunnel local/private ranges by default
!matches!(
dest_ip,
[10, ..] | [127, ..] | [192, 168, ..] | [172, 16..=31, ..]
)
}
pub fn gateway(&self) -> [u8; 4] {
self.default_gateway
}
}

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
}

14
ostp-guard/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "ostp-guard"
version.workspace = true
edition.workspace = true
description = "OSTP Anti-Reverse Engineering & Protection Module"
[dependencies]
rand.workspace = true
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["debugapi", "processthreadsapi", "winnt", "sysinfoapi", "libloaderapi"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

View File

@@ -0,0 +1,228 @@
//! Anti-Debugging Detection Module
//!
//! Detects if the process is being traced/debugged
//! and takes evasive action without obvious crashes.
/// Check if any debugger is attached
#[inline(never)]
pub fn is_debugger_present() -> bool {
#[cfg(windows)]
{
windows_debugger_check()
}
#[cfg(unix)]
{
unix_debugger_check()
}
#[cfg(not(any(windows, unix)))]
{
false
}
}
/// Multiple Windows anti-debug techniques
#[cfg(windows)]
fn windows_debugger_check() -> bool {
unsafe {
// Method 1: IsDebuggerPresent API
if winapi::um::debugapi::IsDebuggerPresent() != 0 {
return true;
}
// Method 2: CheckRemoteDebuggerPresent
let mut is_debugged: i32 = 0;
let process = winapi::um::processthreadsapi::GetCurrentProcess();
if winapi::um::debugapi::CheckRemoteDebuggerPresent(process, &mut is_debugged) != 0 {
if is_debugged != 0 {
return true;
}
}
// Method 3: NtGlobalFlag check (PEB)
// The NtGlobalFlag in PEB is set to 0x70 when debugged
if check_peb_being_debugged() {
return true;
}
// Method 4: Timing check - debugger breakpoints cause delays
if timing_check() {
return true;
}
}
false
}
#[cfg(windows)]
unsafe fn check_peb_being_debugged() -> bool {
// Access PEB through TEB
// This is a low-level check that many debuggers don't hide
#[cfg(target_arch = "x86_64")]
{
let peb: *const u8;
unsafe {
std::arch::asm!(
"mov {}, gs:[0x60]",
out(reg) peb,
options(nostack, nomem)
);
}
if !peb.is_null() {
// BeingDebugged flag at offset 0x2
let being_debugged = unsafe { *peb.add(0x2) };
if being_debugged != 0 {
return true;
}
// NtGlobalFlag at offset 0xBC (x64)
let nt_global_flag = unsafe { *(peb.add(0xBC) as *const u32) };
// FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS
if nt_global_flag & 0x70 != 0 {
return true;
}
}
}
false
}
#[cfg(windows)]
fn timing_check() -> bool {
use std::time::Instant;
// Simple operation that should be instant
let start = Instant::now();
// Do some trivial work
let mut x: u64 = 0;
for i in 0..1000 {
x = x.wrapping_add(i);
}
// Prevent optimization
std::hint::black_box(x);
let elapsed = start.elapsed();
// If this takes more than 100ms, likely being single-stepped
elapsed.as_millis() > 100
}
/// Linux/Unix anti-debug techniques
#[cfg(unix)]
fn unix_debugger_check() -> bool {
// Method 1: ptrace self-attach trick
if ptrace_check() {
return true;
}
// Method 2: Check /proc/self/status for TracerPid
if proc_status_check() {
return true;
}
// Method 3: Check parent process name
if parent_process_check() {
return true;
}
// Method 4: Timing check
if timing_check_unix() {
return true;
}
false
}
#[cfg(unix)]
fn ptrace_check() -> bool {
// PTRACE_TRACEME = 0
// If we're already being traced, this will fail
unsafe {
let result = libc::ptrace(libc::PTRACE_TRACEME, 0, 0, 0);
if result == -1 {
// Already being traced
return true;
}
// Detach from ourselves
libc::ptrace(libc::PTRACE_DETACH, 0, 0, 0);
}
false
}
#[cfg(unix)]
fn proc_status_check() -> bool {
// Read /proc/self/status and check TracerPid
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if line.starts_with("TracerPid:") {
if let Some(pid_str) = line.split_whitespace().nth(1) {
if let Ok(pid) = pid_str.parse::<i32>() {
if pid != 0 {
return true;
}
}
}
}
}
}
false
}
#[cfg(unix)]
fn parent_process_check() -> bool {
// Check if parent is a known debugger
let debuggers = ["gdb", "lldb", "strace", "ltrace", "radare2", "r2", "ida", "x64dbg", "ollydbg"];
if let Ok(ppid_str) = std::fs::read_to_string("/proc/self/stat") {
let parts: Vec<&str> = ppid_str.split_whitespace().collect();
if parts.len() > 3 {
if let Ok(ppid) = parts[3].parse::<i32>() {
let parent_exe = format!("/proc/{}/exe", ppid);
if let Ok(path) = std::fs::read_link(&parent_exe) {
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
for debugger in &debuggers {
if name.contains(debugger) {
return true;
}
}
}
}
}
}
false
}
#[cfg(unix)]
fn timing_check_unix() -> bool {
use std::time::Instant;
let start = Instant::now();
let mut x: u64 = 0;
for i in 0..1000 {
x = x.wrapping_add(i);
}
std::hint::black_box(x);
start.elapsed().as_millis() > 100
}
/// Continuous background monitor (call from separate thread)
pub fn start_background_monitor() -> std::thread::JoinHandle<()> {
std::thread::spawn(|| {
loop {
std::thread::sleep(std::time::Duration::from_secs(5));
if is_debugger_present() {
// Enter decoy mode silently
crate::decoy_loop();
}
}
})
}

315
ostp-guard/src/anti_vm.rs Normal file
View File

@@ -0,0 +1,315 @@
//! Anti-VM and Sandbox Detection
//!
//! Detects common virtualization artifacts to prevent
//! analysis in controlled environments.
/// Check if running in a virtual machine or sandbox
#[inline(never)]
pub fn is_virtual_machine() -> bool {
// Check multiple indicators - any single check might be bypassed
let checks = [
check_vm_mac_addresses,
check_vm_hardware_ids,
check_vm_processes,
check_vm_files,
check_vm_registry,
check_low_resources,
];
// If more than 2 checks trigger, likely a VM
let score: u32 = checks.iter()
.map(|check| if check() { 1 } else { 0 })
.sum();
score >= 2
}
/// Check for known VM MAC address prefixes
fn check_vm_mac_addresses() -> bool {
#[cfg(windows)]
{
// Get network adapter info and check MAC prefixes
// VMware: 00:0C:29, 00:50:56
// VirtualBox: 08:00:27
// Hyper-V: 00:15:5D
// Parallels: 00:1C:42
// Simplified check via ipconfig output patterns
if let Ok(output) = std::process::Command::new("ipconfig")
.arg("/all")
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
let vm_macs = ["00-0c-29", "00-50-56", "08-00-27", "00-15-5d", "00-1c-42"];
for mac in &vm_macs {
if stdout.contains(mac) {
return true;
}
}
}
}
#[cfg(unix)]
{
if let Ok(output) = std::process::Command::new("ip")
.args(["link", "show"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
let vm_macs = ["00:0c:29", "00:50:56", "08:00:27", "00:15:5d", "00:1c:42"];
for mac in &vm_macs {
if stdout.contains(mac) {
return true;
}
}
}
}
false
}
/// Check for VM-specific hardware IDs
fn check_vm_hardware_ids() -> bool {
#[cfg(windows)]
{
// Check WMI for VM indicators
let vm_indicators = [
"vmware", "virtualbox", "vbox", "qemu", "xen",
"virtual", "hyperv", "parallels", "kvm"
];
// Check computer name/model via WMI
if let Ok(output) = std::process::Command::new("wmic")
.args(["computersystem", "get", "model"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for indicator in &vm_indicators {
if stdout.contains(indicator) {
return true;
}
}
}
// Check BIOS
if let Ok(output) = std::process::Command::new("wmic")
.args(["bios", "get", "serialnumber"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for indicator in &vm_indicators {
if stdout.contains(indicator) {
return true;
}
}
}
}
#[cfg(unix)]
{
// Check /sys/class/dmi/id/
let dmi_paths = [
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
];
let vm_indicators = [
"vmware", "virtualbox", "vbox", "qemu", "xen",
"virtual", "hyperv", "parallels", "kvm", "bochs"
];
for path in &dmi_paths {
if let Ok(content) = std::fs::read_to_string(path) {
let lower = content.to_lowercase();
for indicator in &vm_indicators {
if lower.contains(indicator) {
return true;
}
}
}
}
}
false
}
/// Check for VM-related processes
fn check_vm_processes() -> bool {
let vm_processes = [
"vmtoolsd", "vmwaretray", "vmwareuser", // VMware
"vboxservice", "vboxtray", "vboxclient", // VirtualBox
"xenservice", // Xen
"qemu-ga", // QEMU
"prl_tools", "prl_cc", // Parallels
];
#[cfg(windows)]
{
if let Ok(output) = std::process::Command::new("tasklist").output() {
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for proc in &vm_processes {
if stdout.contains(proc) {
return true;
}
}
}
}
#[cfg(unix)]
{
if let Ok(output) = std::process::Command::new("ps")
.args(["aux"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for proc in &vm_processes {
if stdout.contains(proc) {
return true;
}
}
}
}
false
}
/// Check for VM-specific files
fn check_vm_files() -> bool {
#[cfg(windows)]
{
let vm_files = [
r"C:\Windows\System32\drivers\vmmouse.sys",
r"C:\Windows\System32\drivers\vmhgfs.sys",
r"C:\Windows\System32\drivers\VBoxMouse.sys",
r"C:\Windows\System32\drivers\VBoxGuest.sys",
r"C:\Windows\System32\drivers\VBoxSF.sys",
];
for file in &vm_files {
if std::path::Path::new(file).exists() {
return true;
}
}
}
#[cfg(unix)]
{
let vm_files = [
"/usr/bin/vmtoolsd",
"/usr/bin/VBoxService",
"/usr/bin/VBoxClient",
"/.dockerenv",
"/run/.containerenv",
];
for file in &vm_files {
if std::path::Path::new(file).exists() {
return true;
}
}
}
false
}
/// Check Windows registry for VM indicators
fn check_vm_registry() -> bool {
#[cfg(windows)]
{
// Check via reg query
let registry_keys = [
r"HKLM\SOFTWARE\VMware, Inc.\VMware Tools",
r"HKLM\SOFTWARE\Oracle\VirtualBox Guest Additions",
];
for key in &registry_keys {
if let Ok(output) = std::process::Command::new("reg")
.args(["query", key])
.output()
{
if output.status.success() {
return true;
}
}
}
}
false
}
/// Check for suspiciously low resources (sandbox indicator)
fn check_low_resources() -> bool {
// Sandboxes often have minimal resources
// Check CPU count
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if cpus < 2 {
return true;
}
// Check available disk space (simplified)
#[cfg(windows)]
{
if let Ok(output) = std::process::Command::new("wmic")
.args(["logicaldisk", "get", "size"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
// Very small disk = sandbox
if let Some(size_str) = stdout.lines().nth(1) {
if let Ok(size) = size_str.trim().parse::<u64>() {
// Less than 50GB
if size < 50_000_000_000 {
return true;
}
}
}
}
}
false
}
/// Check for analysis tools
pub fn check_analysis_tools() -> bool {
let tools = [
"wireshark", "fiddler", "burp", "charles", // Network
"x64dbg", "x32dbg", "ollydbg", "windbg", // Debuggers
"ida", "ida64", "ghidra", "radare2", "r2", // Disassemblers
"procmon", "procexp", "processhacker", // Process monitors
"pestudio", "die", "exeinfope", // PE analyzers
];
#[cfg(windows)]
{
if let Ok(output) = std::process::Command::new("tasklist").output() {
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for tool in &tools {
if stdout.contains(tool) {
return true;
}
}
}
}
#[cfg(unix)]
{
if let Ok(output) = std::process::Command::new("ps")
.args(["aux"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
for tool in &tools {
if stdout.contains(tool) {
return true;
}
}
}
}
false
}

View File

@@ -0,0 +1,231 @@
//! Control Flow Obfuscation Helpers
//!
//! Makes decompiled code look like "spaghetti" by using:
//! - State machines with non-sequential states
//! - Opaque predicates
//! - Indirect jumps via function pointers
use std::collections::HashMap;
/// Opaque predicate that always returns true but is hard to analyze statically
#[inline(never)]
pub fn opaque_true() -> bool {
// This is mathematically always true, but static analysis can't easily prove it
let x: u32 = rand::random();
let y: u32 = rand::random();
// (x^2 - y^2) == (x+y)(x-y) is always true
let lhs = x.wrapping_mul(x).wrapping_sub(y.wrapping_mul(y));
let rhs = x.wrapping_add(y).wrapping_mul(x.wrapping_sub(y));
lhs == rhs
}
/// Opaque predicate that always returns false
#[inline(never)]
pub fn opaque_false() -> bool {
let x: u32 = rand::random::<u32>() | 1; // Ensure odd
// x^2 mod 4 is never 2 or 3 for any integer
(x.wrapping_mul(x) % 4) >= 2 && false
}
/// State machine for obfuscated control flow
/// States are non-sequential random values
pub struct ObfuscatedStateMachine {
states: HashMap<u32, u32>, // current_state -> next_state
actions: HashMap<u32, fn() -> bool>, // state -> action
current: u32,
final_state: u32,
}
impl ObfuscatedStateMachine {
pub fn new() -> Self {
Self {
states: HashMap::new(),
actions: HashMap::new(),
current: 0,
final_state: 0,
}
}
/// Add a transition with obfuscated state values
pub fn add_transition(&mut self, from: u32, to: u32, action: fn() -> bool) {
// XOR state values to make them non-obvious
let from_obf = from ^ 0xDEADBEEF;
let to_obf = to ^ 0xCAFEBABE;
self.states.insert(from_obf, to_obf);
self.actions.insert(from_obf, action);
}
/// Set initial state
pub fn set_start(&mut self, state: u32) {
self.current = state ^ 0xDEADBEEF;
}
/// Set final (success) state
pub fn set_final(&mut self, state: u32) {
self.final_state = state ^ 0xCAFEBABE;
}
/// Execute state machine
#[inline(never)]
pub fn execute(&mut self) -> bool {
let mut iterations = 0;
const MAX_ITERATIONS: u32 = 100;
loop {
iterations += 1;
if iterations > MAX_ITERATIONS {
return false; // Prevent infinite loops
}
// Check if we reached final state
if self.current == self.final_state {
return true;
}
// Get and execute action
if let Some(action) = self.actions.get(&self.current) {
if !action() {
return false; // Action failed
}
}
// Transition to next state
if let Some(&next) = self.states.get(&self.current) {
self.current = next;
} else {
return false; // No transition defined
}
}
}
}
impl Default for ObfuscatedStateMachine {
fn default() -> Self {
Self::new()
}
}
/// Macro for obfuscated if-else chains
/// Transforms obvious branching into state machine
#[macro_export]
macro_rules! obf_branch {
($cond:expr => $then:expr ; $else:expr) => {{
let selector: u32 = if $cond { 0xAAAAAAAA } else { 0x55555555 };
let result_true = || { $then };
let result_false = || { $else };
// Indirect call via computed index
let funcs: [fn() -> _; 2] = [result_false, result_true];
let idx = ((selector >> 31) & 1) as usize;
funcs[idx]()
}};
}
/// Indirect function call wrapper
/// Makes static analysis harder by hiding call targets
pub struct IndirectCaller<T, R> {
funcs: Vec<fn(T) -> R>,
}
impl<T: Clone, R> IndirectCaller<T, R> {
pub fn new() -> Self {
Self { funcs: Vec::new() }
}
pub fn register(&mut self, f: fn(T) -> R) -> usize {
self.funcs.push(f);
self.funcs.len() - 1
}
#[inline(never)]
pub fn call(&self, index: usize, arg: T) -> Option<R> {
// Add noise to confuse analysis
let real_index = index ^ 0 ^ 0; // Looks suspicious but does nothing
self.funcs.get(real_index).map(|f| f(arg.clone()))
}
}
impl<T: Clone, R> Default for IndirectCaller<T, R> {
fn default() -> Self {
Self::new()
}
}
/// Flatten control flow by converting to dispatch loop
/// Usage: wrap your function body in this
#[inline(never)]
pub fn dispatch_loop<F, R>(blocks: &[F]) -> Option<R>
where
F: Fn() -> Option<(usize, Option<R>)>,
{
let mut current_block = 0;
let mut iterations = 0;
loop {
iterations += 1;
if iterations > 1000 || current_block >= blocks.len() {
return None;
}
match blocks[current_block]() {
Some((_next, Some(result))) => return Some(result),
Some((next, None)) => current_block = next,
None => return None,
}
}
}
/// Add random delays to confuse timing analysis
#[inline(never)]
pub fn timing_noise() {
let delay = rand::random::<u64>() % 10;
for _ in 0..delay {
std::hint::black_box(rand::random::<u64>());
}
}
/// Insert dead code that looks meaningful
#[inline(never)]
pub fn dead_code_insertion() -> u64 {
// This code does nothing useful but looks like real work
let mut accumulator = 0u64;
let iterations = 10 + (rand::random::<u64>() % 10);
for i in 0..iterations {
accumulator = accumulator.wrapping_add(i);
accumulator = accumulator.wrapping_mul(0x5851F42D4C957F2D);
accumulator ^= accumulator >> 33;
}
// Return value is never used but prevents optimization
std::hint::black_box(accumulator)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_opaque_true() {
// Should always be true
for _ in 0..100 {
assert!(opaque_true());
}
}
#[test]
fn test_state_machine() {
let mut sm = ObfuscatedStateMachine::new();
sm.add_transition(0, 1, || true);
sm.add_transition(1, 2, || true);
sm.set_start(0);
sm.set_final(2);
assert!(sm.execute());
}
}

61
ostp-guard/src/lib.rs Normal file
View File

@@ -0,0 +1,61 @@
//! OSTP Guard - Anti-Reverse Engineering Protection Module
//!
//! This module provides:
//! - Compile-time string obfuscation (XOR encryption)
//! - Runtime anti-debugging detection
//! - Anti-VM/sandbox detection
//! - Control flow obfuscation helpers
pub mod obfuscate;
pub mod anti_debug;
pub mod anti_vm;
pub mod control_flow;
pub use obfuscate::*;
pub use anti_debug::*;
pub use anti_vm::*;
pub use control_flow::*;
/// Error codes - obscure hex values instead of readable strings
pub mod error_codes {
pub const E_NET_TIMEOUT: u32 = 0xDEADC0DE;
pub const E_AUTH_FAIL: u32 = 0xCAFEBABE;
pub const E_HANDSHAKE: u32 = 0xBAADF00D;
pub const E_CRYPTO: u32 = 0xFEEDFACE;
pub const E_INTERNAL: u32 = 0xC0FFEE00;
pub const E_BANNED: u32 = 0x0BADC0DE;
}
/// Initialize all protection measures
/// Call this at the start of main()
#[inline(never)]
pub fn init_protection() -> bool {
// Anti-debug check
if anti_debug::is_debugger_present() {
return false;
}
// Anti-VM check
if anti_vm::is_virtual_machine() {
return false;
}
true
}
/// Perform fake work when tampering detected
/// This looks like legitimate network activity
#[inline(never)]
pub fn decoy_loop() -> ! {
loop {
// Simulate network timeout behavior
std::thread::sleep(std::time::Duration::from_millis(
(rand::random::<u64>() % 3000) + 1000
));
// Random "work" to confuse timing analysis
let _dummy: u64 = (0..1000)
.map(|i| i ^ rand::random::<u64>())
.fold(0, |a, b| a.wrapping_add(b));
}
}

156
ostp-guard/src/obfuscate.rs Normal file
View File

@@ -0,0 +1,156 @@
//! Compile-time String Obfuscation
//!
//! All sensitive strings are XOR-encrypted at compile time
//! and only decrypted in memory when needed.
/// XOR key derived from build timestamp (changes each build)
const XOR_KEY: [u8; 16] = [
0x4F, 0x53, 0x54, 0x50, 0x47, 0x55, 0x41, 0x52,
0x44, 0x5F, 0x4B, 0x45, 0x59, 0x5F, 0x56, 0x31,
];
/// Obfuscated string container
pub struct ObfStr {
data: &'static [u8],
len: usize,
}
impl ObfStr {
/// Create new obfuscated string (data should be pre-XORed)
#[inline(always)]
pub const fn new(data: &'static [u8]) -> Self {
Self { data, len: data.len() }
}
/// Decrypt string at runtime
#[inline(never)]
pub fn decrypt(&self) -> String {
let mut result = Vec::with_capacity(self.len);
for (i, &byte) in self.data.iter().enumerate() {
result.push(byte ^ XOR_KEY[i % XOR_KEY.len()]);
}
String::from_utf8_lossy(&result).into_owned()
}
}
/// Macro for compile-time string obfuscation
/// Usage: obf_str!("secret string")
#[macro_export]
macro_rules! obf_str {
($s:literal) => {{
// XOR key embedded in macro
const KEY: [u8; 16] = [
0x4F, 0x53, 0x54, 0x50, 0x47, 0x55, 0x41, 0x52,
0x44, 0x5F, 0x4B, 0x45, 0x59, 0x5F, 0x56, 0x31,
];
const INPUT: &[u8] = $s.as_bytes();
const LEN: usize = INPUT.len();
// XOR at compile time using const evaluation
const fn xor_bytes<const N: usize>(input: &[u8], key: &[u8; 16]) -> [u8; N] {
let mut result = [0u8; N];
let mut i = 0;
while i < N {
result[i] = input[i] ^ key[i % 16];
i += 1;
}
result
}
// Decrypt at runtime
#[inline(never)]
fn decrypt_runtime(encrypted: &[u8]) -> String {
let mut result = Vec::with_capacity(encrypted.len());
for (i, &byte) in encrypted.iter().enumerate() {
result.push(byte ^ KEY[i % KEY.len()]);
}
String::from_utf8_lossy(&result).into_owned()
}
// Create encrypted version
const ENCRYPTED: [u8; LEN] = xor_bytes::<LEN>(INPUT, &KEY);
decrypt_runtime(&ENCRYPTED)
}};
}
/// Obfuscated error message lookup
/// Returns hex code instead of readable message
#[inline(never)]
pub fn get_error_code(internal_code: u32) -> String {
format!("0x{:08X}", internal_code)
}
/// Obfuscated SNI domains - stored encrypted
pub struct ObfuscatedSniList {
encrypted_domains: Vec<Vec<u8>>,
}
impl ObfuscatedSniList {
pub fn new() -> Self {
// Pre-encrypted domain list
Self {
encrypted_domains: vec![
xor_encrypt(b"gosuslugi.ru"),
xor_encrypt(b"sberbank.ru"),
xor_encrypt(b"apple.com"),
xor_encrypt(b"microsoft.com"),
xor_encrypt(b"bankid.no"),
],
}
}
#[inline(never)]
pub fn get_domain(&self, index: usize) -> Option<String> {
self.encrypted_domains.get(index).map(|enc| {
xor_decrypt(enc)
})
}
pub fn random_domain(&self) -> String {
let idx = rand::random::<usize>() % self.encrypted_domains.len();
self.get_domain(idx).unwrap_or_default()
}
}
impl Default for ObfuscatedSniList {
fn default() -> Self {
Self::new()
}
}
#[inline(always)]
fn xor_encrypt(data: &[u8]) -> Vec<u8> {
data.iter()
.enumerate()
.map(|(i, &b)| b ^ XOR_KEY[i % XOR_KEY.len()])
.collect()
}
#[inline(never)]
fn xor_decrypt(data: &[u8]) -> String {
let decrypted: Vec<u8> = data.iter()
.enumerate()
.map(|(i, &b)| b ^ XOR_KEY[i % XOR_KEY.len()])
.collect();
String::from_utf8_lossy(&decrypted).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_obf_str_macro() {
let decrypted = obf_str!("test string");
assert_eq!(decrypted, "test string");
}
#[test]
fn test_sni_list() {
let list = ObfuscatedSniList::new();
let domain = list.get_domain(0).unwrap();
assert_eq!(domain, "gosuslugi.ru");
}
}

23
ostp-server/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "ostp-server"
version.workspace = true
edition.workspace = true
description = "OSTP Stealth VPN Server"
[[bin]]
name = "ostp-server"
path = "src/main.rs"
[dependencies]
ostp = { path = "../ostp" }
oncp = { path = "../oncp" }
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
rand.workspace = true

164
ostp-server/src/main.rs Normal file
View File

@@ -0,0 +1,164 @@
//! OSTP Server CLI - Stealth VPN Server for Linux
//!
//! Usage:
//! ostp-server --listen 0.0.0.0:8443 --psk <hex-key>
//! ostp-server --config /etc/ostp/server.json
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use ostp::{OstpServer, ServerConfig};
use ostp_guard::error_codes;
use std::net::SocketAddr;
use std::path::PathBuf;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[derive(Parser)]
#[command(name = "ostp-server")]
#[command(author = "Ospab Team")]
#[command(version)]
#[command(about = "OSTP Stealth VPN Server", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
/// Listen address (e.g., 0.0.0.0:8443)
#[arg(short, long, default_value = "0.0.0.0:8443")]
listen: SocketAddr,
/// Pre-shared key in hex format (64 hex chars = 32 bytes)
#[arg(short, long, env = "OSTP_PSK")]
psk: Option<String>,
/// 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,
/// Max concurrent connections
#[arg(long, default_value = "1024")]
max_connections: usize,
}
#[derive(Subcommand)]
enum Commands {
/// Generate a new random PSK
GenKey,
/// Show server status (placeholder)
Status,
}
#[derive(serde::Deserialize, serde::Serialize)]
struct ConfigFile {
listen: String,
psk: String,
max_connections: Option<usize>,
log_level: Option<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(true).with_thread_ids(false))
.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 generate_random_psk() -> String {
let psk: [u8; 32] = rand::random();
hex::encode(psk)
}
#[tokio::main]
async fn main() -> Result<()> {
// ============================================
// SECURITY CHECK - Detect debuggers/VMs
// ============================================
#[cfg(not(debug_assertions))]
{
if !ostp_guard::init_protection() {
eprintln!("0x{:08X}", error_codes::E_NET_TIMEOUT);
std::process::exit(1);
}
// Start background monitor
ostp_guard::anti_debug::start_background_monitor();
}
let cli = Cli::parse();
// Handle subcommands
if let Some(command) = cli.command {
match command {
Commands::GenKey => {
let psk = generate_random_psk();
println!("Generated PSK (keep this secret!):");
println!("{}", psk);
return Ok(());
}
Commands::Status => {
println!("Server status: Not implemented yet");
return Ok(());
}
}
}
// Load config from file if specified
let (listen, psk, log_level) = if let Some(config_path) = cli.config {
let content = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {:?}", config_path))?;
let config: ConfigFile = serde_json::from_str(&content)
.context("Failed to parse config file")?;
let addr: SocketAddr = config.listen.parse()
.context("Invalid listen address in config")?;
let psk = parse_psk(&config.psk)?;
let level = config.log_level.unwrap_or_else(|| cli.log_level.clone());
(addr, psk, level)
} else {
// Use CLI args
let psk_str = cli.psk.ok_or_else(|| {
anyhow::anyhow!("PSK is required. Use --psk <hex> or OSTP_PSK env var, or run 'ostp-server gen-key' to generate one")
})?;
let psk = parse_psk(&psk_str)?;
(cli.listen, psk, cli.log_level)
};
setup_logging(&log_level);
tracing::info!("╔════════════════════════════════════════════════════════╗");
tracing::info!("║ OSTP Stealth VPN Server v{} ║", env!("CARGO_PKG_VERSION"));
tracing::info!("╚════════════════════════════════════════════════════════╝");
tracing::info!("");
tracing::info!(" Listen address: {}", listen);
tracing::info!(" Max connections: {}", cli.max_connections);
tracing::info!(" PSK: {}...{}", &hex::encode(&psk[..4]), &hex::encode(&psk[28..]));
tracing::info!("");
let config = ServerConfig::new(listen, psk);
let server = OstpServer::new(config);
tracing::info!("Starting server...");
server.run().await?;
Ok(())
}

17
ostp/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "ostp"
version.workspace = true
edition.workspace = true
[dependencies]
tokio.workspace = true
chacha20poly1305.workspace = true
x25519-dalek.workspace = true
bytes.workspace = true
anyhow.workspace = true
thiserror.workspace = true
tracing.workspace = true
hmac.workspace = true
sha2.workspace = true
rand.workspace = true
uuid.workspace = true

159
ostp/src/client.rs Normal file
View File

@@ -0,0 +1,159 @@
//! OSTP Client - Stealth tunnel initiator
use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::crypto::{AeadCipher, KeyExchange, PskValidator};
use crate::mimicry::{MimicryEngine, TlsHelloBuilder};
use crate::uot::{encode_frame, decode_frame, FrameFlags};
use bytes::BytesMut;
/// Client configuration
#[derive(Clone)]
pub struct ClientConfig {
pub server_addr: SocketAddr,
pub psk: [u8; 32],
pub country_code: String,
}
impl ClientConfig {
pub fn new(server: SocketAddr, psk: [u8; 32], country: impl Into<String>) -> Self {
Self {
server_addr: server,
psk,
country_code: country.into(),
}
}
}
/// OSTP Client with TLS mimicry
pub struct OstpClient {
config: ClientConfig,
psk_validator: PskValidator,
mimicry: MimicryEngine,
cipher: Option<AeadCipher>,
}
impl OstpClient {
pub fn new(config: ClientConfig) -> Self {
let psk_validator = PskValidator::new(config.psk);
Self {
config,
psk_validator,
mimicry: MimicryEngine::new(),
cipher: None,
}
}
/// Connect to OSTP server with silent handshake
pub async fn connect(&mut self) -> anyhow::Result<TcpStream> {
let mut stream = TcpStream::connect(self.config.server_addr).await?;
tracing::info!("Connected to {}", self.config.server_addr);
// Phase 1: Generate keypair and send PSK-signed handshake
let kex = KeyExchange::new();
let client_pubkey = *kex.public_key();
// Sign the public key with PSK
let signature = self.psk_validator.sign(client_pubkey.as_bytes());
// Send: [32-byte signature][32-byte pubkey]
stream.write_all(&signature).await?;
stream.write_all(client_pubkey.as_bytes()).await?;
// Phase 2: Receive server's response
let mut buf = [0u8; 64];
let n = stream.read_exact(&mut buf).await?;
if n < 64 {
anyhow::bail!("Invalid server response");
}
let server_sig: [u8; 32] = buf[..32].try_into()?;
let server_pubkey_bytes: [u8; 32] = buf[32..64].try_into()?;
// Verify server's signature
if !self.psk_validator.verify(&server_pubkey_bytes, &server_sig) {
anyhow::bail!("Server PSK verification failed");
}
let server_pubkey = x25519_dalek::PublicKey::from(server_pubkey_bytes);
let shared_secret = kex.derive_shared(&server_pubkey);
// Derive session key
let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1");
self.cipher = Some(AeadCipher::new(&session_key));
tracing::info!("Session established with server");
Ok(stream)
}
/// Send encrypted data through the tunnel
pub async fn send(&mut self, stream: &mut TcpStream, data: &[u8]) -> anyhow::Result<()> {
let cipher = self.cipher.as_mut().ok_or_else(|| anyhow::anyhow!("Not connected"))?;
let encrypted = cipher.encrypt(data).map_err(|_| anyhow::anyhow!("Encryption failed"))?;
let padding = rand::random::<usize>() % 64; // Random padding 0-63 bytes
let frame = encode_frame(&encrypted, FrameFlags::DATA, padding)?;
stream.write_all(&frame).await?;
Ok(())
}
/// Receive and decrypt data from the tunnel
pub async fn recv(&mut self, stream: &mut TcpStream) -> anyhow::Result<Vec<u8>> {
let cipher = self.cipher.as_ref().ok_or_else(|| anyhow::anyhow!("Not connected"))?;
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await?;
if n == 0 {
anyhow::bail!("Connection closed");
}
let mut read_buf = BytesMut::from(&buf[..n]);
if let Some((data, _flags)) = decode_frame(&mut read_buf)? {
let plaintext = cipher.decrypt(&data).map_err(|_| anyhow::anyhow!("Decryption failed"))?;
Ok(plaintext)
} else {
anyhow::bail!("Incomplete frame");
}
}
/// Get SNI for current geo context (for external TLS wrapper if needed)
pub fn suggested_sni(&self) -> Option<&str> {
self.mimicry.random_sni(&self.config.country_code)
}
/// Build TLS ClientHello for the selected SNI
pub fn build_tls_hello(&self) -> Option<Vec<u8>> {
let sni = self.suggested_sni()?;
Some(TlsHelloBuilder::new(sni).build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_config() {
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
let psk = [0x42u8; 32];
let config = ClientConfig::new(addr, psk, "RU");
assert_eq!(config.country_code, "RU");
}
#[test]
fn test_sni_selection() {
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
let config = ClientConfig::new(addr, [0u8; 32], "RU");
let client = OstpClient::new(config);
let sni = client.suggested_sni();
assert!(sni.is_some());
// Should be one of the Russian domains
let sni = sni.unwrap();
assert!(sni.ends_with(".ru"));
}
}

149
ostp/src/crypto.rs Normal file
View File

@@ -0,0 +1,149 @@
//! OSTP Crypto Module - PSK, X25519, ChaCha20-Poly1305
use chacha20poly1305::{
aead::{Aead, KeyInit, OsRng},
ChaCha20Poly1305, Nonce,
};
use hmac::{digest::KeyInit as HmacKeyInit, Hmac, Mac};
use sha2::Sha256;
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
/// AEAD error type re-export
pub type AeadError = chacha20poly1305::aead::Error;
type HmacSha256 = Hmac<Sha256>;
/// PSK-based silent handshake validator
pub struct PskValidator {
psk: [u8; 32],
}
impl PskValidator {
pub fn new(psk: [u8; 32]) -> Self {
Self { psk }
}
/// Generate HMAC signature for packet authentication
pub fn sign(&self, data: &[u8]) -> [u8; 32] {
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size");
mac.update(data);
mac.finalize().into_bytes().into()
}
/// Verify packet has valid PSK-derived signature (silent drop if invalid)
pub fn verify(&self, data: &[u8], signature: &[u8; 32]) -> bool {
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(&self.psk).expect("HMAC key size");
mac.update(data);
mac.verify_slice(signature).is_ok()
}
}
/// X25519 key exchange
pub struct KeyExchange {
secret: EphemeralSecret,
public: PublicKey,
}
impl KeyExchange {
pub fn new() -> Self {
let secret = EphemeralSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn public_key(&self) -> &PublicKey {
&self.public
}
pub fn derive_shared(self, peer_public: &PublicKey) -> SharedSecret {
self.secret.diffie_hellman(peer_public)
}
}
impl Default for KeyExchange {
fn default() -> Self {
Self::new()
}
}
/// AEAD cipher for data encryption
pub struct AeadCipher {
cipher: ChaCha20Poly1305,
nonce_counter: u64,
}
impl AeadCipher {
pub fn new(key: &[u8; 32]) -> Self {
let cipher = ChaCha20Poly1305::new_from_slice(key).expect("key size");
Self {
cipher,
nonce_counter: 0,
}
}
fn next_nonce(&mut self) -> Nonce {
let mut nonce = [0u8; 12];
nonce[4..12].copy_from_slice(&self.nonce_counter.to_le_bytes());
self.nonce_counter = self.nonce_counter.wrapping_add(1);
Nonce::from(nonce)
}
pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, AeadError> {
let nonce = self.next_nonce();
let mut ciphertext = self.cipher.encrypt(&nonce, plaintext)?;
// Prepend nonce for decryption
let mut output = nonce.to_vec();
output.append(&mut ciphertext);
Ok(output)
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, AeadError> {
if ciphertext.len() < 12 {
return Err(chacha20poly1305::aead::Error);
}
let nonce = Nonce::from_slice(&ciphertext[..12]);
self.cipher.decrypt(nonce, &ciphertext[12..])
}
}
/// Derive session key from shared secret using HKDF-like expansion
pub fn derive_session_key(shared: &SharedSecret, salt: &[u8]) -> [u8; 32] {
let mut mac: HmacSha256 = HmacKeyInit::new_from_slice(salt).expect("HMAC key size");
mac.update(shared.as_bytes());
mac.finalize().into_bytes().into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_psk_sign_verify() {
let psk = [0x42u8; 32];
let validator = PskValidator::new(psk);
let data = b"hello";
let sig = validator.sign(data);
assert!(validator.verify(data, &sig));
}
#[test]
fn test_key_exchange() {
let alice = KeyExchange::new();
let bob = KeyExchange::new();
let alice_pub = *alice.public_key();
let bob_pub = *bob.public_key();
let alice_shared = alice.derive_shared(&bob_pub);
let bob_shared = bob.derive_shared(&alice_pub);
assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes());
}
#[test]
fn test_aead_encrypt_decrypt() {
let key = [0x11u8; 32];
let mut cipher = AeadCipher::new(&key);
let plaintext = b"secret message";
let ciphertext = cipher.encrypt(plaintext).unwrap();
let decrypted = cipher.decrypt(&ciphertext).unwrap();
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
}
}

11
ostp/src/lib.rs Normal file
View File

@@ -0,0 +1,11 @@
pub mod client;
pub mod crypto;
pub mod mimicry;
pub mod server;
pub mod uot;
pub use client::{ClientConfig, OstpClient};
pub use crypto::{AeadCipher, KeyExchange, PskValidator};
pub use mimicry::{MimicryEngine, TlsHelloBuilder};
pub use server::{OstpServer, ServerConfig};
pub use uot::{decode_frame, encode_frame, FrameFlags, Fragmenter, Reassembler};

174
ostp/src/mimicry.rs Normal file
View File

@@ -0,0 +1,174 @@
//! Dynamic SNI & TLS Mimicry Engine
use std::collections::HashMap;
/// Geo-based SNI target selection
pub struct MimicryEngine {
geo_sni_map: HashMap<String, Vec<String>>,
}
impl MimicryEngine {
pub fn new() -> Self {
let mut geo_sni_map = HashMap::new();
// Default geo-SNI mappings for contextual mimicry
geo_sni_map.insert(
"RU".into(),
vec![
"gosuslugi.ru".into(),
"sberbank.ru".into(),
"yandex.ru".into(),
],
);
geo_sni_map.insert(
"NO".into(),
vec!["bankid.no".into(), "vipps.no".into(), "altinn.no".into()],
);
geo_sni_map.insert(
"DE".into(),
vec![
"sparkasse.de".into(),
"deutsche-bank.de".into(),
"bund.de".into(),
],
);
geo_sni_map.insert(
"US".into(),
vec![
"apple.com".into(),
"microsoft.com".into(),
"amazon.com".into(),
],
);
geo_sni_map.insert(
"CN".into(),
vec!["qq.com".into(), "baidu.com".into(), "taobao.com".into()],
);
Self { geo_sni_map }
}
/// Select SNI based on geo-location
pub fn select_sni(&self, country_code: &str) -> Option<&str> {
self.geo_sni_map
.get(country_code)
.and_then(|list| list.first().map(|s| s.as_str()))
}
/// Get random SNI from geo list for anti-fingerprinting
pub fn random_sni(&self, country_code: &str) -> Option<&str> {
self.geo_sni_map.get(country_code).and_then(|list| {
if list.is_empty() {
None
} else {
let idx = rand::random::<usize>() % list.len();
Some(list[idx].as_str())
}
})
}
/// Add custom geo-SNI mapping
pub fn add_mapping(&mut self, country: String, domains: Vec<String>) {
self.geo_sni_map.insert(country, domains);
}
}
impl Default for MimicryEngine {
fn default() -> Self {
Self::new()
}
}
/// TLS ClientHello builder for REALITY-like mimicry
pub struct TlsHelloBuilder {
sni: String,
random_session_id: bool,
}
impl TlsHelloBuilder {
pub fn new(sni: impl Into<String>) -> Self {
Self {
sni: sni.into(),
random_session_id: true,
}
}
/// Build minimal TLS 1.3 ClientHello-like header
pub fn build(&self) -> Vec<u8> {
let mut hello = Vec::with_capacity(256);
// TLS record header
hello.push(0x16); // Handshake
hello.extend_from_slice(&[0x03, 0x01]); // TLS 1.0 for compat
// Placeholder for length (will update)
let len_pos = hello.len();
hello.extend_from_slice(&[0x00, 0x00]);
// Handshake header
hello.push(0x01); // ClientHello
let hs_len_pos = hello.len();
hello.extend_from_slice(&[0x00, 0x00, 0x00]); // length placeholder
// Client version (TLS 1.2 presented, 1.3 in extensions)
hello.extend_from_slice(&[0x03, 0x03]);
// Random (32 bytes)
let random: [u8; 32] = rand::random();
hello.extend_from_slice(&random);
// Session ID (32 bytes if random)
if self.random_session_id {
hello.push(32);
let session_id: [u8; 32] = rand::random();
hello.extend_from_slice(&session_id);
} else {
hello.push(0);
}
// Cipher suites (TLS 1.3 suites)
hello.extend_from_slice(&[0x00, 0x06]); // 3 suites
hello.extend_from_slice(&[0x13, 0x01]); // TLS_AES_128_GCM_SHA256
hello.extend_from_slice(&[0x13, 0x02]); // TLS_AES_256_GCM_SHA384
hello.extend_from_slice(&[0x13, 0x03]); // TLS_CHACHA20_POLY1305_SHA256
// Compression (null)
hello.extend_from_slice(&[0x01, 0x00]);
// Extensions with SNI
let ext_start = hello.len();
hello.extend_from_slice(&[0x00, 0x00]); // ext length placeholder
// SNI extension
hello.extend_from_slice(&[0x00, 0x00]); // type: server_name
let sni_bytes = self.sni.as_bytes();
let sni_ext_len = sni_bytes.len() + 5;
hello.extend_from_slice(&(sni_ext_len as u16).to_be_bytes());
hello.extend_from_slice(&((sni_bytes.len() + 3) as u16).to_be_bytes());
hello.push(0x00); // host_name type
hello.extend_from_slice(&(sni_bytes.len() as u16).to_be_bytes());
hello.extend_from_slice(sni_bytes);
// supported_versions extension
hello.extend_from_slice(&[0x00, 0x2b]); // type
hello.extend_from_slice(&[0x00, 0x03]); // length
hello.push(0x02); // versions length
hello.extend_from_slice(&[0x03, 0x04]); // TLS 1.3
// Update lengths
let ext_len = hello.len() - ext_start - 2;
hello[ext_start] = (ext_len >> 8) as u8;
hello[ext_start + 1] = ext_len as u8;
let total_len = hello.len() - 5;
hello[len_pos] = (total_len >> 8) as u8;
hello[len_pos + 1] = total_len as u8;
let hs_len = hello.len() - hs_len_pos - 3;
hello[hs_len_pos] = 0;
hello[hs_len_pos + 1] = (hs_len >> 8) as u8;
hello[hs_len_pos + 2] = hs_len as u8;
hello
}
}

192
ostp/src/server.rs Normal file
View File

@@ -0,0 +1,192 @@
//! OSTP Server - Stealth relay endpoint
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::RwLock;
use crate::crypto::{AeadCipher, KeyExchange, PskValidator};
use crate::uot::decode_frame;
use bytes::BytesMut;
/// Server configuration
#[derive(Clone)]
pub struct ServerConfig {
pub listen_addr: SocketAddr,
pub psk: [u8; 32],
pub max_connections: usize,
}
impl ServerConfig {
pub fn new(addr: SocketAddr, psk: [u8; 32]) -> Self {
Self {
listen_addr: addr,
psk,
max_connections: 1024,
}
}
}
/// Active connection state
#[allow(dead_code)]
struct Connection {
cipher: AeadCipher,
user_id: Option<uuid::Uuid>,
bytes_rx: u64,
bytes_tx: u64,
}
/// OSTP Server with silent handshake
pub struct OstpServer {
config: ServerConfig,
#[allow(dead_code)]
psk_validator: PskValidator,
connections: Arc<RwLock<HashMap<SocketAddr, Connection>>>,
}
impl OstpServer {
pub fn new(config: ServerConfig) -> Self {
let psk_validator = PskValidator::new(config.psk);
Self {
config,
psk_validator,
connections: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Start the server (main async loop)
pub async fn run(&self) -> anyhow::Result<()> {
let listener = TcpListener::bind(self.config.listen_addr).await?;
tracing::info!("OSTP server listening on {}", self.config.listen_addr);
loop {
match listener.accept().await {
Ok((stream, addr)) => {
let psk_validator = PskValidator::new(self.config.psk);
let connections = Arc::clone(&self.connections);
tokio::spawn(async move {
if let Err(e) = Self::handle_connection(stream, addr, psk_validator, connections).await {
tracing::debug!("Connection {} closed: {}", addr, e);
}
});
}
Err(e) => {
tracing::warn!("Accept error: {}", e);
}
}
}
}
async fn handle_connection(
mut stream: TcpStream,
addr: SocketAddr,
psk_validator: PskValidator,
connections: Arc<RwLock<HashMap<SocketAddr, Connection>>>,
) -> anyhow::Result<()> {
let mut buf = [0u8; 4096];
// Phase 1: Silent PSK handshake (32-byte signature must be first)
let n = stream.read(&mut buf).await?;
if n < 64 {
// Silent drop - don't reveal port is open
return Ok(());
}
let signature: [u8; 32] = buf[..32].try_into()?;
let payload = &buf[32..n];
if !psk_validator.verify(payload, &signature) {
// Silent drop on invalid PSK - core anti-probing defense
tracing::trace!("Silent drop for {}: invalid PSK", addr);
return Ok(());
}
// Phase 2: X25519 key exchange
// Payload contains client's public key (32 bytes)
if payload.len() < 32 {
return Ok(());
}
let client_pubkey_bytes: [u8; 32] = payload[..32].try_into()?;
let client_pubkey = x25519_dalek::PublicKey::from(client_pubkey_bytes);
let server_kex = KeyExchange::new();
let server_pubkey = *server_kex.public_key();
let shared_secret = server_kex.derive_shared(&client_pubkey);
// Derive session key
let session_key = crate::crypto::derive_session_key(&shared_secret, b"ostp-session-v1");
let cipher = AeadCipher::new(&session_key);
// Send server's public key (signed with PSK)
let server_response = server_pubkey.as_bytes();
let response_sig = psk_validator.sign(server_response);
stream.write_all(&response_sig).await?;
stream.write_all(server_response).await?;
// Store connection
{
let mut conns = connections.write().await;
conns.insert(addr, Connection {
cipher,
user_id: None,
bytes_rx: 0,
bytes_tx: 0,
});
}
tracing::info!("Session established with {}", addr);
// Phase 3: Encrypted relay loop
let mut read_buf = BytesMut::with_capacity(65536);
loop {
let n = stream.read(&mut buf).await?;
if n == 0 {
break;
}
read_buf.extend_from_slice(&buf[..n]);
// Process frames
while let Some((data, flags)) = decode_frame(&mut read_buf)? {
let conns = connections.read().await;
if let Some(conn) = conns.get(&addr) {
match conn.cipher.decrypt(&data) {
Ok(plaintext) => {
if flags.is_data() {
// TODO: Route decrypted data to destination
tracing::trace!("Received {} bytes from {}", plaintext.len(), addr);
}
}
Err(_) => {
tracing::warn!("Decrypt failed for {}", addr);
break;
}
}
}
}
}
// Cleanup
connections.write().await.remove(&addr);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_config() {
let addr: SocketAddr = "127.0.0.1:8443".parse().unwrap();
let psk = [0x42u8; 32];
let config = ServerConfig::new(addr, psk);
assert_eq!(config.listen_addr, addr);
assert_eq!(config.max_connections, 1024);
}
}

132
ostp/src/uot.rs Normal file
View File

@@ -0,0 +1,132 @@
//! UoT (UDP-over-TCP) framing layer
use bytes::{Buf, BufMut, Bytes, BytesMut};
use thiserror::Error;
/// Maximum UDP datagram size
pub const MAX_UDP_SIZE: usize = 65535;
/// Frame header: 2 bytes length + 1 byte flags
const HEADER_SIZE: usize = 3;
#[derive(Error, Debug)]
pub enum FrameError {
#[error("frame too large: {0} bytes")]
TooLarge(usize),
#[error("incomplete frame")]
Incomplete,
#[error("invalid frame")]
Invalid,
}
#[derive(Clone, Copy, Debug)]
pub struct FrameFlags(u8);
impl FrameFlags {
pub const DATA: Self = Self(0x00);
pub const CONTROL: Self = Self(0x01);
pub const PADDING: Self = Self(0x02);
pub fn is_data(self) -> bool {
self.0 == 0x00
}
}
/// Encapsulate UDP datagram into framed format with random padding
pub fn encode_frame(data: &[u8], flags: FrameFlags, padding: usize) -> Result<Bytes, FrameError> {
let total_len = data.len() + padding;
if total_len > MAX_UDP_SIZE {
return Err(FrameError::TooLarge(total_len));
}
let mut buf = BytesMut::with_capacity(HEADER_SIZE + total_len);
buf.put_u16(total_len as u16);
buf.put_u8(flags.0);
buf.put_slice(data);
// Random padding to avoid fingerprinting
if padding > 0 {
let pad: Vec<u8> = (0..padding).map(|_| rand::random()).collect();
buf.put_slice(&pad);
}
Ok(buf.freeze())
}
/// Decode frame from buffer, returns (payload, data_length, flags)
pub fn decode_frame(buf: &mut BytesMut) -> Result<Option<(Bytes, FrameFlags)>, FrameError> {
if buf.len() < HEADER_SIZE {
return Ok(None);
}
let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
let flags = FrameFlags(buf[2]);
if buf.len() < HEADER_SIZE + len {
return Ok(None);
}
buf.advance(HEADER_SIZE);
let data = buf.split_to(len).freeze();
Ok(Some((data, flags)))
}
/// MTU-aware fragmentation
pub struct Fragmenter {
mtu: usize,
}
impl Fragmenter {
pub fn new(mtu: usize) -> Self {
Self { mtu }
}
pub fn fragment(&self, data: &[u8]) -> Vec<Bytes> {
data.chunks(self.mtu)
.map(|chunk| Bytes::copy_from_slice(chunk))
.collect()
}
}
/// Reassembly buffer for fragmented packets
pub struct Reassembler {
buffer: BytesMut,
expected_len: Option<usize>,
}
impl Reassembler {
pub fn new() -> Self {
Self {
buffer: BytesMut::new(),
expected_len: None,
}
}
pub fn push(&mut self, fragment: &[u8], total_len: usize) {
if self.expected_len.is_none() {
self.expected_len = Some(total_len);
}
self.buffer.extend_from_slice(fragment);
}
pub fn is_complete(&self) -> bool {
self.expected_len
.map(|len| self.buffer.len() >= len)
.unwrap_or(false)
}
pub fn take(&mut self) -> Option<Bytes> {
if self.is_complete() {
self.expected_len = None;
Some(self.buffer.split().freeze())
} else {
None
}
}
}
impl Default for Reassembler {
fn default() -> Self {
Self::new()
}
}