1422 lines
53 KiB
Rust
1422 lines
53 KiB
Rust
// 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<Child>,
|
||
/// Path to the .ovpn configuration file
|
||
config_path: PathBuf,
|
||
/// The external IP address assigned by this VPN
|
||
external_ip: Option<String>,
|
||
/// Baseline (pre-VPN) external IP for verification
|
||
baseline_ip: Option<String>,
|
||
/// 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<PathBuf>,
|
||
/// Path to temporary modified .ovpn file for this instance (if any)
|
||
temp_config_path: Option<PathBuf>,
|
||
/// Path to OpenVPN log file created for this instance (if any)
|
||
log_path: Option<PathBuf>,
|
||
}
|
||
|
||
impl VpnInstance {
|
||
/// Creates a new VPN instance without starting the connection
|
||
pub fn new(
|
||
config_path: PathBuf,
|
||
username: String,
|
||
password: String,
|
||
baseline_ip: Option<String>,
|
||
) -> Result<Self> {
|
||
// 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::<Vec<_>>()
|
||
.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<bool> {
|
||
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<bool> {
|
||
#[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::<Vec<_>>().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<bool> {
|
||
#[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<bool, anyhow::Error> {
|
||
//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<PathBuf> {
|
||
// 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<Option<String>> {
|
||
// Empfohlen: Nutze einen shared Client (z. B. aus Arc<reqwest::Client>)
|
||
// Falls du keinen hast, ist das hier immer noch besser als jedes Mal neu bauen:
|
||
static CLIENT: once_cell::sync::Lazy<reqwest::Client> = 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<bool> {
|
||
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<bool> {
|
||
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<PathBuf> {
|
||
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<Arc<Mutex<VpnInstance>>>,
|
||
current_index: Arc<Mutex<usize>>,
|
||
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<Self> {
|
||
// 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<String> = 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<String> = match client
|
||
.get("https://api.ipify.org?format=json")
|
||
.send()
|
||
.await
|
||
{
|
||
Ok(resp) => {
|
||
if resp.status().is_success() {
|
||
if let Ok(json) = resp.json::<serde_json::Value>().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<Arc<Mutex<VpnInstance>>> {
|
||
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<Mutex<VpnInstance>>) -> 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<usize> {
|
||
#[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<usize> {
|
||
#[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")
|
||
} |