// src/scraper/vpn_manager.rs use anyhow::{Context, Result, anyhow}; use fantoccini::client; use serde_json; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::process::{Child, Command}; use tokio::sync::Mutex; use tokio::time::{sleep, timeout, Duration}; use std::process::Stdio; use reqwest::Client; use tokio::fs as tokio_fs; use std::time::{SystemTime, UNIX_EPOCH}; use crate::logger; /// Represents a single OpenVPN connection with its associated state pub struct VpnInstance { /// The OpenVPN process handle process: Option, /// Path to the .ovpn configuration file config_path: PathBuf, /// The external IP address assigned by this VPN external_ip: Option, /// Baseline (pre-VPN) external IP for verification baseline_ip: Option, /// Hostname derived from the config file (e.g., "ca149.vpnbook.com") hostname: String, /// Number of tasks completed in the current session tasks_completed: usize, /// VPN credentials username: String, password: String, /// Health status is_healthy: bool, /// Path to credentials file created for this instance (if any) cred_path: Option, /// Path to temporary modified .ovpn file for this instance (if any) temp_config_path: Option, /// Path to OpenVPN log file created for this instance (if any) log_path: Option, } impl VpnInstance { /// Creates a new VPN instance without starting the connection pub fn new( config_path: PathBuf, username: String, password: String, baseline_ip: Option, ) -> Result { // Use the file stem (filename without extension) as the hostname/identifier. // This avoids using the parent directory name which can be the same // for many configs and cause collisions when copying into config-auto. let hostname = config_path .file_stem() .and_then(|n| n.to_str()) .unwrap_or("unknown") .to_string(); Ok(Self { process: None, config_path, external_ip: None, baseline_ip, hostname, tasks_completed: 0, username, password, is_healthy: false, cred_path: None, temp_config_path: None, log_path: None, }) } /// Starts the OpenVPN connection and detects the assigned IP pub async fn connect(&mut self) -> Result<()> { crate::util::logger::log_info(&format!("Starting VPN connection for {}", self.hostname)).await; // Create fixed config first //let fixed_config_path = self.create_fixed_config().await // .context("Failed to create fixed OpenVPN config")?; // Store the temp config path so we can clean it up later self.temp_config_path = Some(self.config_path.clone()); let cred_file = self.create_credentials_file().await?; self.cred_path = Some(cred_file.clone()); // Use baseline IP supplied when pool was created, otherwise detect once let baseline_ip = if self.baseline_ip.is_some() { self.baseline_ip.clone() } else { let b = self.detect_external_ip().await.ok().flatten(); if let Some(ref ip) = b { crate::util::logger::log_info(&format!("Baseline IP (before VPN): {}", ip)).await; } b }; self.baseline_ip = baseline_ip; #[cfg(target_os = "windows")] { // Windows: Spawn individual openvpn.exe process for this instance let temp_dir = std::env::temp_dir(); let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let instance_config = temp_dir.join(format!("ovpn_{}_{}.ovpn", self.hostname, nanos)); let log_path = temp_dir.join(format!("openvpn_{}_{}.log", self.hostname, nanos)); // Copy and modify the .ovpn file to include auth-user-pass (async) let ovpn_content = tokio_fs::read_to_string(&self.config_path).await?; let mut modified_content = ovpn_content; // Remove existing auth-user-pass lines if present modified_content = modified_content .lines() .filter(|line| !line.trim().starts_with("auth-user-pass")) .collect::>() .join("\n"); // Add auth-user-pass pointing to credentials file at the end modified_content.push('\n'); let cred_path_str = cred_file.to_string_lossy().replace('\\', "/"); modified_content.push_str(&format!("auth-user-pass \"{}\"\n", cred_path_str)); tokio_fs::write(&instance_config, modified_content).await?; self.temp_config_path = Some(instance_config.clone()); self.log_path = Some(log_path.clone()); // Verify OpenVPN executable exists let openvpn_exe = r"C:\Program Files\OpenVPN\bin\openvpn.exe"; if !std::path::Path::new(openvpn_exe).exists() { return Err(anyhow!( "OpenVPN executable not found at: {}\nPlease install OpenVPN from: https://openvpn.net/community-downloads/", openvpn_exe )); } // Spawn openvpn.exe process directly let mut cmd = Command::new(openvpn_exe); cmd.arg("--config") .arg(&instance_config) .arg("--log") .arg(&log_path) .stdout(Stdio::piped()) .stderr(Stdio::piped()); crate::util::logger::log_info(&format!( "Spawning OpenVPN: {} --config {} --log {}", openvpn_exe, instance_config.display(), log_path.display() )).await; let mut process = cmd .spawn() .context("Failed to spawn openvpn.exe. Ensure OpenVPN is installed.")?; // Log OpenVPN stdout in background if let Some(stdout) = process.stdout.take() { let hostname = self.hostname.clone(); tokio::spawn(async move { use tokio::io::AsyncBufReadExt; let mut reader = tokio::io::BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { crate::util::logger::log_info(&format!("OpenVPN-OUT [{}]: {}", hostname, line)).await; } }); } // Store process handle self.process = Some(process); // Wait for OpenVPN to initialize and verify connection // Initial wait: OpenVPN takes ~8-10 seconds typically crate::util::logger::log_info(&format!( "Waiting for OpenVPN initialization ({})", self.hostname )).await; tokio::time::sleep(Duration::from_secs(40)).await; for i in 0..3 { if self.scan_openvpn_logs().await? { break; } crate::util::logger::log_info("OpenVPN not initialized yet, waiting 10 seconds...").await; tokio::time::sleep(Duration::from_secs(10)).await; } // Now run comprehensive verification with retries built-in match self.verify_vpn_connection().await { Ok(true) => { self.is_healthy = true; crate::util::logger::log_info(&format!( "✓ VPN {} connected successfully with IP: {}", self.hostname, self.external_ip.as_ref().unwrap_or(&"unknown".to_string()) )).await; return Ok(()); } Ok(false) => { // Verification failed - kill process if let Some(mut p) = self.process.take() { let _ = p.kill().await; } return Err(anyhow!( "VPN connection verification failed for {}", self.hostname )); } Err(e) => { // Error during verification if let Some(mut p) = self.process.take() { let _ = p.kill().await; } return Err(anyhow!( "VPN verification error for {}: {}", self.hostname, e )); } } } #[cfg(not(target_os = "windows"))] { // Non-Windows implementation let mut cmd = Command::new("openvpn"); cmd.arg("--config") .arg(&self.config_path) .arg("--auth-user-pass") .arg(&cred_file) .arg("--daemon") .stdout(Stdio::piped()) .stderr(Stdio::piped()); match cmd.spawn() { Err(e) if e.kind() == std::io::ErrorKind::NotFound => { return Err(anyhow!( "OpenVPN not found. Please install it:\n \ Linux: sudo apt-get install openvpn\n \ macOS: brew install openvpn\n \ Error: {}", e )); } Err(e) => { return Err(anyhow!("Failed to spawn OpenVPN: {}", e)); } Ok(mut process) => { // Capture output for monitoring if let Some(stdout) = process.stdout.take() { let hostname = self.hostname.clone(); tokio::spawn(async move { use tokio::io::AsyncBufReadExt; let mut reader = tokio::io::BufReader::new(stdout).lines(); while let Ok(Some(line)) = reader.next_line().await { crate::util::logger::log_info(&format!("[VPN-{}] {}", hostname, line)).await; } }); } self.process = Some(process); } } // Wait for connection and verify (Unix) sleep(Duration::from_secs(10)).await; match self.verify_vpn_connection().await { Ok(true) => { self.is_healthy = true; crate::util::logger::log_info(&format!( "✓ VPN {} connected successfully", self.hostname )).await; return Ok(()); } Ok(false) => { self.disconnect().await?; return Err(anyhow!( "Failed to verify VPN connection for {}", self.hostname )); } Err(e) => { self.disconnect().await?; return Err(e); } } } } async fn verify_vpn_connection(&mut self) -> Result { logger::log_info(&format!("Verifying VPN connection for {}", self.hostname)).await; // 1. Quick process check if self.process.is_none() { logger::log_warn("Process check failed - no process").await; return Ok(false); } // 2. Check logs for success markers (with retries for file buffering) let mut log_success = false; for attempt in 1..=5 { tokio::time::sleep(Duration::from_secs(2)).await; // Wait for log flush match self.scan_openvpn_logs().await { Ok(true) => { log_success = true; logger::log_info(&format!( "✓ Log verification passed (attempt {})", attempt )).await; break; } Ok(false) => { if attempt < 5 { logger::log_info(&format!( "Log verification pending (attempt {}/5)...", attempt )).await; } } Err(e) => { logger::log_warn(&format!("Log read error: {}", e)).await; } } } if !log_success { logger::log_warn("Log verification failed after 5 attempts").await; return Ok(false); } // 3. Wait for routes to be applied (happens ~2 seconds after "Initialization Sequence Completed") logger::log_info("Waiting for routes to stabilize...").await; tokio::time::sleep(Duration::from_secs(3)).await; // 4. Network route verification (with retries) let mut route_ok = false; for attempt in 1..=3 { if self.check_routing_table().await.unwrap_or(false) { route_ok = true; logger::log_info(&format!( "✓ Route verification passed (attempt {})", attempt )).await; break; } if attempt < 3 { logger::log_info(&format!( "Route verification pending (attempt {}/3)...", attempt )).await; tokio::time::sleep(Duration::from_secs(2)).await; } } if !route_ok { logger::log_warn("Route verification failed - routes may not be established").await; // Don't fail yet - some VPNs work without perfect routing table state } // 5. External IP change verification (with retries and longer timeout) logger::log_info("Verifying IP change...").await; for attempt in 1..=3 { match timeout(Duration::from_secs(8), self.verify_ip_change()).await { Ok(Ok(true)) => { let ip = self.external_ip.as_ref().unwrap(); logger::log_info(&format!( "✓ VPN connection verified - New IP: {} (attempt {})", ip, attempt )).await; return Ok(true); } Ok(Ok(false)) => { if attempt < 3 { logger::log_info(&format!( "IP verification pending (attempt {}/3)...", attempt )).await; tokio::time::sleep(Duration::from_secs(2)).await; } } Ok(Err(e)) => { logger::log_warn(&format!("IP check error: {}", e)).await; } Err(_) => { logger::log_warn("IP check timeout").await; } } } logger::log_error(&format!( "✗ VPN connection verification failed for {} - IP unchanged after 3 attempts", self.hostname )).await; Ok(false) } // Improved routing table check with better Windows detection async fn check_routing_table(&self) -> Result { #[cfg(target_os = "windows")] { use std::process::Command; let output = Command::new("cmd") .args(&["/C", "route", "print", "0.0.0.0"]) .output() .context("Failed to execute route print")?; if !output.status.success() { return Ok(false); } let output_str = String::from_utf8_lossy(&output.stdout); // Check for VPN-related routing entries // Look for: TAP adapter in routes, or 10.x.x.x destinations (common VPN subnets) let has_vpn_routes = output_str.contains("TAP") || output_str.contains("10.8.0") // Common OpenVPN subnet || output_str.contains("10.9.0") // Another common subnet || output_str.to_lowercase().contains("openvpn"); // Also check for 0.0.0.0/128.0.0.0 split routing (redirect-gateway) let has_redirect = output_str.contains("128.0.0.0") && output_str.matches("0.0.0.0").count() >= 2; let result = has_vpn_routes || has_redirect; if result { logger::log_info("✓ Routing table shows VPN routes").await; } else { logger::log_warn("✗ No VPN routes detected in routing table").await; logger::log_info(&format!("Route output preview: {}", &output_str.lines().take(10).collect::>().join("\n") )).await; } Ok(result) } #[cfg(not(target_os = "windows"))] { // For non-Windows, just check if we can detect IP change // (routing is less critical to verify on Unix) Ok(true) } } // Improved log scanning with better error handling async fn scan_openvpn_logs(&self) -> Result { #[cfg(target_os = "windows")] { if let Some(ref log_path) = self.log_path { // Try to read the file, but handle "not found" gracefully let content = match tokio::fs::read_to_string(log_path).await { Ok(c) => c, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { // Log file doesn't exist yet return Ok(false); } Err(e) => { return Err(anyhow::anyhow!("Failed to read log file: {}", e)); } }; // Success marker if content.contains("Initialization Sequence Completed") { // Check for fatal errors that might have occurred after success if content.contains("Exiting due to fatal error") { return Ok(false); } // Check for excessive retries/errors let error_count = content.matches("TLS Error").count() + content.matches("Connection reset").count() + content.matches("SIGTERM").count() + content.matches("Restart pause").count(); if error_count > 3 { logger::log_warn(&format!( "Logs show {} errors despite initialization complete", error_count )).await; return Ok(false); } return Ok(true); } // Check for immediate fatal errors if content.contains("Cannot open TUN/TAP dev") || content.contains("Exiting due to fatal error") || content.contains("AUTH: Received control message: AUTH_FAILED") { return Ok(false); } } } Ok(false) } // Improved IP verification with better error handling async fn verify_ip_change(&mut self) -> Result { //let detected_ip = self.detect_external_ip().await?; let new_ip = self .detect_external_ip() .await .context("Failed to query external IP after VPN connection")? .with_context(|| "VPN connected, but no external IP received – tunnel broken?")?; // Compare with baseline if let Some(baseline) = &self.baseline_ip { if &new_ip == baseline { logger::log_warn(&format!( "IP unchanged from baseline: {} (VPN may not be routing traffic)", baseline )).await; return Ok(false); } logger::log_info(&format!( "IP changed: {} → {} ✓", baseline, new_ip )).await; } else { logger::log_info(&format!("Detected IP: {}", new_ip)).await; } self.external_ip = Some(new_ip); Ok(true) } /// Creates a fixed version of the OpenVPN config to work with modern OpenVPN async fn create_fixed_config(&self) -> Result { // Read the original config let content = tokio::fs::read_to_string(&self.config_path) .await .context("Failed to read OpenVPN config")?; // Create a temporary file for the fixed config let temp_dir = std::env::temp_dir(); let temp_config_path = temp_dir.join(format!("fixed_{}.ovpn", self.hostname.replace(|c: char| !c.is_alphanumeric(), "_"))); let mut fixed_lines = Vec::new(); let mut has_data_ciphers = false; let mut has_compression_setting = false; // Process each line for line in content.lines() { let trimmed = line.trim(); if trimmed.starts_with("cipher ") { // Skip old cipher line, we'll add data-ciphers later if needed continue; } else if trimmed.starts_with("data-ciphers") { has_data_ciphers = true; fixed_lines.push(line.to_string()); } else if trimmed.contains("allow-compression") { has_compression_setting = true; fixed_lines.push(line.to_string()); } else if trimmed.starts_with(";") || trimmed.starts_with("#") { // Keep comments fixed_lines.push(line.to_string()); } else if !trimmed.is_empty() { fixed_lines.push(line.to_string()); } } // Add modern cipher suite if missing if !has_data_ciphers { fixed_lines.push("data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305".to_string()); } // Add compression setting if missing if !has_compression_setting { fixed_lines.push("allow-compression no".to_string()); } // Remove any duplicate 'auth-user-pass' lines (we'll add our own later) fixed_lines.retain(|line| !line.trim().starts_with("auth-user-pass")); // Write the fixed config let fixed_content = fixed_lines.join("\n"); tokio::fs::write(&temp_config_path, fixed_content) .await .context("Failed to write fixed OpenVPN config")?; Ok(temp_config_path) } /// Disconnects the VPN connection pub async fn disconnect(&mut self) -> Result<()> { #[cfg(target_os = "windows")] { // Kill the openvpn.exe process if it exists if let Some(mut process) = self.process.take() { crate::util::logger::log_info(&format!("Disconnecting VPN {}", self.hostname)).await; let _ = process.kill().await; // Clean up temp config file and log file if present if let Some(ref p) = self.temp_config_path { let _ = tokio_fs::remove_file(p).await; } if let Some(ref lp) = self.log_path { let _ = tokio_fs::remove_file(lp).await; } } // Remove credential file if present if let Some(ref cred) = self.cred_path { let _ = tokio_fs::remove_file(cred).await; } self.external_ip = None; self.is_healthy = false; self.tasks_completed = 0; Ok(()) } #[cfg(not(target_os = "windows"))] { if let Some(mut process) = self.process.take() { crate::util::logger::log_info(&format!("Disconnecting VPN {}", self.hostname)).await; process.kill().await.context("Failed to kill OpenVPN process")?; self.external_ip = None; self.is_healthy = false; self.tasks_completed = 0; } // Remove credentials file if present if let Some(ref cred) = self.cred_path { let _ = tokio_fs::remove_file(cred).await; } Ok(()) } } /// Reconnects the VPN (disconnect + connect) pub async fn reconnect(&mut self) -> Result<()> { crate::util::logger::log_info(&format!("Reconnecting VPN {}", self.hostname)).await; self.disconnect().await?; sleep(Duration::from_secs(10)).await; // Brief delay between disconnect and reconnect self.connect().await } /// Ermittelt die aktuelle externe IPv4-Adresse über ipify.org. /// /// Nutzt einen wiederverwendbaren reqwest-Client aus dem Pool (vermutlich vorhandenen) /// shared Client-Pool oder erstellt einen mit sinnvollen Defaults. /// /// # Returns /// - `Ok(Some(ip))` bei erfolgreicher Erkennung /// - `Ok(None)` wenn der Server antwortet, aber kein gültiger IP-String kommt /// - `Err(_)` bei Netzwerk-, Timeout- oder Parse-Fehlern (wird mit Kontext angereichert) async fn detect_external_ip(&self) -> anyhow::Result> { // Empfohlen: Nutze einen shared Client (z. B. aus Arc) // Falls du keinen hast, ist das hier immer noch besser als jedes Mal neu bauen: static CLIENT: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { reqwest::Client::builder() .timeout(std::time::Duration::from_secs(8)) .user_agent("my-scraper/1.0") .build() .expect("Failed to build static reqwest client") }); let resp = CLIENT .get("https://api.ipify.org?format=json") .send() .await .context("Failed to reach ipify.org – no internet or DNS issue")?; // 4xx/5xx → Fehler let resp = resp .error_for_status() .context("ipify.org returned error status")?; #[derive(serde::Deserialize)] struct IpResponse { ip: String, } let ip_obj: IpResponse = resp .json() .await .context("Failed to parse JSON from ipify.org")?; // Einfache Validierung: sieht nach IPv4 aus? if ip_obj.ip.contains('.') && !ip_obj.ip.is_empty() { Ok(Some(ip_obj.ip)) } else { Ok(None) } } /// Performs a simple health check without reconnection pub async fn health_check(&mut self) -> Result { if self.process.is_none() { self.is_healthy = false; return Ok(false); } match self.detect_external_ip().await { Ok(Some(_)) => { self.is_healthy = true; Ok(true) } _ => { self.is_healthy = false; Ok(false) } } } /// Performs a health check and automatically reconnects if failed pub async fn health_check_with_reconnect(&mut self) -> Result { if self.process.is_none() { self.is_healthy = false; return Ok(false); } // Try to detect IP again match self.detect_external_ip().await { Ok(Some(ip)) => { if let Some(ref current_ip) = self.external_ip { if &ip != current_ip { crate::util::logger::log_warn(&format!( "VPN {} IP changed unexpectedly: {} -> {}", self.hostname, current_ip, ip )).await; self.external_ip = Some(ip); } } self.is_healthy = true; Ok(true) } _ => { crate::util::logger::log_warn(&format!( "Health check failed for VPN {}, attempting reconnect...", self.hostname )).await; self.is_healthy = false; // Attempt automatic reconnection match self.reconnect().await { Ok(()) => { crate::util::logger::log_info(&format!( "✓ VPN {} reconnected successfully", self.hostname )).await; Ok(true) } Err(e) => { crate::util::logger::log_error(&format!( "✗ VPN {} reconnection failed: {}", self.hostname, e )).await; Ok(false) } } } } } /// Creates a temporary credentials file for OpenVPN authentication async fn create_credentials_file(&self) -> Result { let temp_dir = std::env::temp_dir(); let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let cred_path = temp_dir.join(format!("vpn_creds_{}_{}.txt", self.hostname, nanos)); let content = format!("{}\n{}\n", self.username, self.password); tokio::fs::write(&cred_path, content) .await .context("Failed to write credentials file")?; Ok(cred_path) } /// Increments the task counter and returns whether rotation is needed pub fn increment_task_count(&mut self, tasks_per_session: usize) -> bool { self.tasks_completed += 1; if tasks_per_session > 0 && self.tasks_completed >= tasks_per_session { true } else { false } } /// Returns the external IP if connected pub fn external_ip(&self) -> Option<&str> { self.external_ip.as_deref() } /// Returns the hostname pub fn hostname(&self) -> &str { &self.hostname } /// Returns whether the VPN is healthy pub fn is_healthy(&self) -> bool { self.is_healthy } /// Resets the task counter pub fn reset_task_count(&mut self) { self.tasks_completed = 0; } } impl Drop for VpnInstance { fn drop(&mut self) { if let Some(mut process) = self.process.take() { let _ = process.start_kill(); } } } /// Manages a pool of VPN instances for rotation pub struct VpnPool { instances: Vec>>, current_index: Arc>, enable_rotation: bool, tasks_per_session: usize, } impl VpnPool { /// Creates a new VPN pool from .ovpn configuration files /// Automatically ensures sufficient TAP adapters are installed pub async fn new( ovpn_dir: &Path, username: String, password: String, enable_rotation: bool, tasks_per_session: usize, amount_of_openvpn_servers: usize, ) -> Result { // STEP 1: Ensure we have enough TAP adapters (auto-install if needed) #[cfg(target_os = "windows")] { crate::util::logger::log_info("=== TAP Adapter Check ===").await; let _ = Self::ensure_tap_adapters(amount_of_openvpn_servers).await?; crate::util::logger::log_info("=== TAP Adapter Check Complete ===").await; } // STEP 2: Continue with normal VPN pool initialization let mut ovpn_files = Vec::new(); // Recursively find all .ovpn files let mut entries = tokio::fs::read_dir(ovpn_dir).await?; while let Some(entry) = entries.next_entry().await? { let path = entry.path(); if path.is_dir() { let mut sub_entries = tokio::fs::read_dir(&path).await?; while let Some(sub_entry) = sub_entries.next_entry().await? { let sub_path = sub_entry.path(); if sub_path.extension().and_then(|s| s.to_str()) == Some("ovpn") && is_tcp_443_config(&sub_path) { ovpn_files.push(sub_path); } } } else if path.extension().and_then(|s| s.to_str()) == Some("ovpn") && is_tcp_443_config(&path) { ovpn_files.push(path); } } // Deduplicate configs by server let mut unique = Vec::new(); use std::collections::HashSet; let mut seen: HashSet = HashSet::new(); for cfg in ovpn_files { match tokio_fs::read_to_string(&cfg).await { Ok(contents) => { let mut server_key = None; for line in contents.lines() { let l = line.trim(); if l.starts_with('#') || l.is_empty() { continue; } if l.starts_with("remote ") { let parts: Vec<&str> = l.split_whitespace().collect(); if parts.len() >= 2 { let host = parts[1]; let port = if parts.len() >= 3 { parts[2] } else { "" }; let key = if port.is_empty() { host.to_string() } else { format!("{}:{}", host, port) }; server_key = Some(key); break; } } } let key = server_key.unwrap_or_else(|| { cfg.file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown") .to_string() }); if !seen.contains(&key) { seen.insert(key); unique.push(cfg); } } Err(_) => { unique.push(cfg); } } } ovpn_files = unique; if ovpn_files.is_empty() { return Err(anyhow!("No .ovpn files found in {:?}", ovpn_dir)); } crate::util::logger::log_info(&format!( "Found {} OpenVPN configurations", ovpn_files.len() )).await; // Build shared HTTP client let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .pool_max_idle_per_host(8) .build() .context("Failed to build HTTP client for IP detection")?; // Detect baseline IP let baseline_ip: Option = match client .get("https://api.ipify.org?format=json") .send() .await { Ok(resp) => { if resp.status().is_success() { if let Ok(json) = resp.json::().await { json.get("ip") .and_then(|v| v.as_str()) .map(|s| s.to_string()) } else { None } } else { None } } Err(_) => None, }; if let Some(ref ip) = baseline_ip { crate::util::logger::log_info(&format!("Baseline IP for pool: {}", ip)).await; } // Create VPN instances let mut instances = Vec::new(); for config_path in ovpn_files { let instance = VpnInstance::new( config_path, username.clone(), password.clone(), baseline_ip.clone(), )?; instances.push(Arc::new(Mutex::new(instance))); } Ok(Self { instances, current_index: Arc::new(Mutex::new(0)), enable_rotation, tasks_per_session, }) } /// Connects all VPN instances in parallel (much faster than sequential) pub async fn connect_all(&self) -> Result<()> { crate::util::logger::log_info("Connecting all VPN instances in batches...").await; let start_time = std::time::Instant::now(); // Auto-detect optimal batch size based on available TAP adapters let batch_size = Self::detect_optimal_batch_size().await; let mut connected = 0; let mut failed = 0; // Process instances in batches for (batch_num, chunk) in self.instances.chunks(batch_size).enumerate() { crate::util::logger::log_info(&format!( "Starting batch {}/{} ({} VPNs)...", batch_num + 1, (self.instances.len() + batch_size - 1) / batch_size, chunk.len() )).await; // Spawn parallel tasks for this batch let mut tasks = Vec::new(); for (i, instance) in chunk.iter().enumerate() { let instance_clone = Arc::clone(instance); let global_index = batch_num * batch_size + i; let task = tokio::spawn(async move { let mut inst = instance_clone.lock().await; let hostname = inst.hostname().to_string(); match inst.connect().await { Ok(_) => { let ip = inst.external_ip().unwrap_or("unknown"); crate::util::logger::log_info(&format!( "✓ VPN {} ({}) connected with IP: {}", global_index + 1, hostname, ip )).await; Ok(()) } Err(e) => { crate::util::logger::log_warn(&format!( "⚠ Failed to connect VPN {} ({}): {}", global_index + 1, hostname, e )).await; Err(e) } } }); tasks.push(task); } // Wait for this batch to complete let results = futures::future::join_all(tasks).await; for result in results { match result { Ok(Ok(_)) => connected += 1, Ok(Err(_)) | Err(_) => failed += 1, } } // Small delay between batches to let TAP adapters stabilize if batch_num < (self.instances.len() + batch_size - 1) / batch_size - 1 { tokio::time::sleep(Duration::from_secs(2)).await; } } let elapsed = start_time.elapsed(); if connected == 0 && failed > 0 { crate::util::logger::log_error(&format!( "✗ FATAL: All {} VPN connection attempts failed in {:.1}s. Make sure OpenVPN is installed:\n Windows: https://openvpn.net/community-downloads/\n Linux: sudo apt-get install openvpn\n macOS: brew install openvpn", failed, elapsed.as_secs_f64() )).await; return Err(anyhow!("Failed to connect any VPN instances. OpenVPN may not be installed.")); } else if failed > 0 { crate::util::logger::log_warn(&format!( "⚠ Connected {} VPN instances successfully, {} failed in {:.1}s (will use available instances)", connected, failed, elapsed.as_secs_f64() )).await; } else { crate::util::logger::log_info(&format!( "✓ All {} VPN instances connected successfully in {:.1}s", connected, elapsed.as_secs_f64() )).await; } Ok(()) } /// Gets the next available VPN instance (round-robin) pub async fn acquire(&self) -> Result>> { let mut index = self.current_index.lock().await; let instance = self.instances[*index % self.instances.len()].clone(); *index += 1; Ok(instance) } /// Rotates a VPN instance if rotation is enabled and threshold is met pub async fn rotate_if_needed(&self, instance: Arc>) -> Result<()> { if !self.enable_rotation { return Ok(()); } let mut inst = instance.lock().await; if inst.increment_task_count(self.tasks_per_session) { crate::util::logger::log_info(&format!( "Task threshold reached for VPN {}, rotating...", inst.hostname() )).await; let old_ip = inst.external_ip().map(|s| s.to_string()); inst.reconnect().await?; let new_ip = inst.external_ip().map(|s| s.to_string()); match (old_ip, new_ip) { (Some(old), Some(new)) if old != new => { crate::util::logger::log_info(&format!( "✓ VPN {} rotated: {} -> {}", inst.hostname(), old, new )).await; } (Some(old), Some(new)) if old == new => { crate::util::logger::log_warn(&format!( "⚠ VPN {} reconnected but IP unchanged: {}", inst.hostname(), old )).await; } _ => { crate::util::logger::log_error(&format!( "✗ VPN {} rotation verification failed", inst.hostname() )).await; } } inst.reset_task_count(); } Ok(()) } /// Performs health checks on all VPN instances with automatic reconnection pub async fn health_check_all_with_reconnect(&self) -> Result<()> { for instance in &self.instances { let mut inst = instance.lock().await; let _ = inst.health_check_with_reconnect().await; } Ok(()) } /// Returns the number of VPN instances pub fn len(&self) -> usize { self.instances.len() } /// Disconnects all VPN instances pub async fn disconnect_all(&self) -> Result<()> { crate::util::logger::log_info("Disconnecting all VPN instances...").await; for instance in &self.instances { let mut inst = instance.lock().await; inst.disconnect().await?; } Ok(()) } /// Amount of taps adapters currently installed /// One VPN connection requires one TAP adapter pub async fn ensure_tap_adapters(amount_of_openvpn_servers: usize) -> Result { #[cfg(target_os = "windows")] { use std::fs; crate::util::logger::log_info("Checking TAP adapter availability...").await; let tap_count = Self::count_tap_adapters().await?; if tap_count >= amount_of_openvpn_servers { crate::util::logger::log_info(&format!( "✓ Found {} TAP adapters - sufficient for parallel VPN connections", tap_count )).await; return Ok(tap_count); } crate::util::logger::log_warn(&format!( "⚠ Only {} TAP adapter(s) found. Installing {} more for optimal performance...", tap_count, amount_of_openvpn_servers - tap_count )).await; // Create PowerShell script let script_content = format!( r#"# Auto-generated TAP adapter installation script # Requires Administrator privileges $ErrorActionPreference = "Stop" # Check if running as Administrator $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) {{ Write-Host "ERROR: This script must be run as Administrator!" -ForegroundColor Red Write-Host "Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow exit 1 }} Write-Host "Installing additional TAP adapters..." -ForegroundColor Cyan $tapctlPath = "C:\Program Files\OpenVPN\bin\tapctl.exe" if (-not (Test-Path $tapctlPath)) {{ Write-Host "ERROR: OpenVPN not found at: $tapctlPath" -ForegroundColor Red Write-Host "Please install OpenVPN from: https://openvpn.net/community-downloads/" -ForegroundColor Yellow exit 1 }} $existingCount = {} $targetCount = 10 for ($i = ($existingCount + 1); $i -le $targetCount; $i++) {{ Write-Host "Creating TAP adapter #$i..." -ForegroundColor Yellow try {{ & $tapctlPath create --name "OpenVPN-TAP-$i" if ($LASTEXITCODE -eq 0) {{ Write-Host " ✓ Created OpenVPN-TAP-$i" -ForegroundColor Green }} else {{ Write-Host " ⚠ Failed to create adapter (exit code: $LASTEXITCODE)" -ForegroundColor Red }} }} catch {{ Write-Host " ✗ Error: $_" -ForegroundColor Red }} Start-Sleep -Milliseconds 500 }} Write-Host "`n✓ TAP adapter installation complete!" -ForegroundColor Green Write-Host "Verifying installation..." -ForegroundColor Cyan $finalCount = (Get-NetAdapter | Where-Object {{ $_.InterfaceDescription -like "*TAP*" }}).Count Write-Host "Total TAP adapters now: $finalCount" -ForegroundColor Cyan exit 0 "#, tap_count ); let script_path = std::env::current_dir()?.join("install_tap_adapters.ps1"); // Write script to file fs::write(&script_path, script_content) .context("Failed to create PowerShell script")?; crate::util::logger::log_info(&format!( "Created installation script: {:?}", script_path )).await; // Execute PowerShell script as Administrator crate::util::logger::log_info( "Executing TAP adapter installation (requires Administrator)..." ).await; let output = Command::new("powershell") .args(&[ "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", script_path.to_str().unwrap() ]) .output().await; match output { Ok(result) => { let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); if !stdout.is_empty() { crate::util::logger::log_info(&format!("Script output:\n{}", stdout)).await; } if result.status.success() { crate::util::logger::log_info( "✓ TAP adapters installed successfully" ).await; // Recount adapters tokio::time::sleep(Duration::from_secs(2)).await; let new_count = Self::count_tap_adapters().await?; crate::util::logger::log_info(&format!( "✓ Total TAP adapters now: {}", new_count )).await; return Ok(new_count); } else { let error_msg = if stderr.contains("Administrator") { "Administrator privileges required. Please run as Administrator." } else if !stderr.is_empty() { &stderr } else { "Unknown error during installation" }; crate::util::logger::log_error(&format!( "✗ TAP adapter installation failed: {}", error_msg )).await; crate::util::logger::log_warn( "Continuing with existing adapters. VPN connections will be sequential." ).await; return Ok(tap_count); } } Err(e) => { crate::util::logger::log_error(&format!( "Failed to execute installation script: {}", e )).await; crate::util::logger::log_info(&format!( "You can manually run: {:?}", script_path )).await; return Ok(tap_count); } } } #[cfg(not(target_os = "windows"))] { // Non-Windows: TAP adapters not needed Ok(10) } } /// Counts existing TAP adapters on the system async fn count_tap_adapters() -> Result { #[cfg(target_os = "windows")] { let output = Command::new("ipconfig") .arg("/all") .output().await .context("Failed to execute ipconfig")?; let output_str = String::from_utf8_lossy(&output.stdout); let count = output_str .lines() .filter(|line| { let lower = line.to_lowercase(); (lower.contains("tap") && lower.contains("adapter")) || lower.contains("tap-windows") }) .count() / 2; // Two lines per adapter Ok(count) } #[cfg(not(target_os = "windows"))] { Ok(10) // Not applicable on non-Windows } } async fn detect_optimal_batch_size() -> usize { #[cfg(target_os = "windows")] { use std::process::Command; // Try to detect TAP adapters via ipconfig match Command::new("ipconfig") .arg("/all") .output() { Ok(output) => { let output_str = String::from_utf8_lossy(&output.stdout); // Count lines containing "TAP" adapter references // Two lines per tap adapter typically let tap_count = output_str .lines() .filter(|line| { let lower = line.to_lowercase(); (lower.contains("tap") && lower.contains("adapter")) || lower.contains("tap-windows") }) .count() / 2; if tap_count == 0 { crate::util::logger::log_warn( "⚠ No TAP adapters detected! VPN connections will likely fail." ).await; return 1; } else if tap_count == 1 { crate::util::logger::log_warn(&format!( "⚠ Only 1 TAP adapter found. VPNs will connect sequentially.\n\ For parallel connections, install more TAP adapters:\n\ Run as Admin: cd 'C:\\Program Files\\OpenVPN\\bin' && .\\tapctl.exe create --name 'OpenVPN-TAP-2'" )).await; return 1; } else { crate::util::logger::log_info(&format!( "✓ Found {} TAP adapters - enabling parallel VPN connections", tap_count )).await; // Use 75% of available adapters to leave some headroom let optimal = ((tap_count as f32 * 0.75).ceil() as usize).max(1); return optimal.min(6); // Cap at 6 for stability } } Err(e) => { crate::util::logger::log_warn(&format!( "⚠ Failed to detect TAP adapters: {}. Using sequential connection.", e )).await; return 1; } } } #[cfg(not(target_os = "windows"))] { // Linux/macOS: TAP adapters aren't a limitation // Can run more in parallel return 4; } } } fn is_tcp_443_config(path: &Path) -> bool { // Get filename as string let filename = match path.file_name().and_then(|n| n.to_str()) { Some(name) => name.to_lowercase(), None => return false, }; // Check if contains both "tcp" and "443" filename.contains("tcp") && filename.contains("443") }